From 99435774675d5d87d1a65671f5d5ea1314b314ce Mon Sep 17 00:00:00 2001 From: Allen Goodman Date: Thu, 3 Oct 2024 16:43:53 -0400 Subject: [PATCH 1/8] graph operators --- src/beignet/__init__.py | 18 + src/beignet/_graph_matrix_to_masked_tensor.py | 11 + src/beignet/_graph_matrix_to_tensor.py | 66 +++ src/beignet/_masked_tensor_to_graph_matrix.py | 33 ++ .../_predecessor_matrix_to_distance_matrix.py | 79 +++ src/beignet/_reconstruct_path.py | 47 ++ src/beignet/_tensor_to_graph_matrix.py | 21 + src/beignet/_tensor_to_masked_graph_matrix.py | 47 ++ src/beignet/_validate_graph_matrix.py | 73 +++ tests/__init__.py | 0 .../test__graph_matrix_to_masked_tensor.py | 0 tests/beignet/test__graph_matrix_to_tensor.py | 0 .../test__masked_tensor_to_graph_matrix.py | 0 ...__predecessor_matrix_to_distance_matrix.py | 0 tests/beignet/test__reconstruct_path.py | 0 tests/beignet/test__tensor_to_graph_matrix.py | 0 .../test__tensor_to_masked_graph_matrix.py | 0 tests/beignet/test__validate_graph_matrix.py | 0 tests/csgraph/__init__.py | 0 tests/csgraph/meson.build | 20 + tests/csgraph/test_connected_components.py | 119 +++++ tests/csgraph/test_conversions.py | 61 +++ tests/csgraph/test_flow.py | 201 ++++++++ tests/csgraph/test_graph_laplacian.py | 368 +++++++++++++ tests/csgraph/test_matching.py | 294 +++++++++++ tests/csgraph/test_pydata_sparse.py | 194 +++++++ tests/csgraph/test_reordering.py | 70 +++ tests/csgraph/test_shortest_path.py | 484 ++++++++++++++++++ tests/csgraph/test_spanning_tree.py | 66 +++ tests/csgraph/test_traversal.py | 81 +++ 30 files changed, 2353 insertions(+) create mode 100644 src/beignet/_graph_matrix_to_masked_tensor.py create mode 100644 src/beignet/_graph_matrix_to_tensor.py create mode 100644 src/beignet/_masked_tensor_to_graph_matrix.py create mode 100644 src/beignet/_predecessor_matrix_to_distance_matrix.py create mode 100644 src/beignet/_reconstruct_path.py create mode 100644 src/beignet/_tensor_to_graph_matrix.py create mode 100644 src/beignet/_tensor_to_masked_graph_matrix.py create mode 100644 src/beignet/_validate_graph_matrix.py create mode 100644 tests/__init__.py create mode 100644 tests/beignet/test__graph_matrix_to_masked_tensor.py create mode 100644 tests/beignet/test__graph_matrix_to_tensor.py create mode 100644 tests/beignet/test__masked_tensor_to_graph_matrix.py create mode 100644 tests/beignet/test__predecessor_matrix_to_distance_matrix.py create mode 100644 tests/beignet/test__reconstruct_path.py create mode 100644 tests/beignet/test__tensor_to_graph_matrix.py create mode 100644 tests/beignet/test__tensor_to_masked_graph_matrix.py create mode 100644 tests/beignet/test__validate_graph_matrix.py create mode 100644 tests/csgraph/__init__.py create mode 100644 tests/csgraph/meson.build create mode 100644 tests/csgraph/test_connected_components.py create mode 100644 tests/csgraph/test_conversions.py create mode 100644 tests/csgraph/test_flow.py create mode 100644 tests/csgraph/test_graph_laplacian.py create mode 100644 tests/csgraph/test_matching.py create mode 100644 tests/csgraph/test_pydata_sparse.py create mode 100644 tests/csgraph/test_reordering.py create mode 100644 tests/csgraph/test_shortest_path.py create mode 100644 tests/csgraph/test_spanning_tree.py create mode 100644 tests/csgraph/test_traversal.py diff --git a/src/beignet/__init__.py b/src/beignet/__init__.py index 4566febcc7..4ae068c70d 100644 --- a/src/beignet/__init__.py +++ b/src/beignet/__init__.py @@ -358,6 +358,16 @@ trim_probabilists_hermite_polynomial_coefficients, ) from .special import error_erf, error_erfc +from ._validate_graph_matrix import validate_graph_matrix +from ._tensor_to_masked_graph_matrix import tensor_to_masked_graph_matrix +from ._tensor_to_graph_matrix import tensor_to_graph_matrix +from ._masked_tensor_to_graph_matrix import masked_tensor_to_graph_matrix +from ._graph_matrix_to_tensor import graph_matrix_to_tensor +from ._graph_matrix_to_masked_tensor import graph_matrix_to_masked_tensor +from ._predecessor_matrix_to_distance_matrix import predecessor_matrix_to_distance_matrix +from ._reconstruct_path import reconstruct_path + + __all__ = [ "add_chebyshev_polynomial", @@ -453,6 +463,8 @@ "gauss_legendre_quadrature", "gauss_physicists_hermite_polynomial_quadrature", "gauss_probabilists_hermite_polynomial_quadrature", + "graph_matrix_to_masked_tensor", + "graph_matrix_to_tensor", "integrate_chebyshev_polynomial", "integrate_laguerre_polynomial", "integrate_legendre_polynomial", @@ -497,6 +509,7 @@ "linear_physicists_hermite_polynomial", "linear_polynomial", "linear_probabilists_hermite_polynomial", + "masked_tensor_to_graph_matrix", "multiply_chebyshev_polynomial", "multiply_chebyshev_polynomial_by_x", "multiply_laguerre_polynomial", @@ -538,6 +551,7 @@ "polynomial_vandermonde_3d", "polynomial_x", "polynomial_zero", + "predecessor_matrix_to_distance_matrix", "probabilists_hermite_polynomial_companion", "probabilists_hermite_polynomial_domain", "probabilists_hermite_polynomial_from_roots", @@ -562,6 +576,7 @@ "random_quaternion", "random_rotation_matrix", "random_rotation_vector", + "reconstruct_path", "rotation_matrix_identity", "rotation_matrix_magnitude", "rotation_matrix_mean", @@ -580,6 +595,8 @@ "subtract_physicists_hermite_polynomial", "subtract_polynomial", "subtract_probabilists_hermite_polynomial", + "tensor_to_graph_matrix", + "tensor_to_masked_graph_matrix", "translation_identity", "trim_chebyshev_polynomial_coefficients", "trim_laguerre_polynomial_coefficients", @@ -587,4 +604,5 @@ "trim_physicists_hermite_polynomial_coefficients", "trim_polynomial_coefficients", "trim_probabilists_hermite_polynomial_coefficients", + "validate_graph_matrix", ] diff --git a/src/beignet/_graph_matrix_to_masked_tensor.py b/src/beignet/_graph_matrix_to_masked_tensor.py new file mode 100644 index 0000000000..53a5134772 --- /dev/null +++ b/src/beignet/_graph_matrix_to_masked_tensor.py @@ -0,0 +1,11 @@ +import numpy +from numpy.ma import MaskedArray +from scipy.sparse import csr_matrix + +from ._graph_matrix_to_tensor import graph_matrix_to_tensor + + +def graph_matrix_to_masked_tensor(input: csr_matrix) -> MaskedArray: + output = graph_matrix_to_tensor(input, numpy.nan) + + return numpy.ma.masked_invalid(output) diff --git a/src/beignet/_graph_matrix_to_tensor.py b/src/beignet/_graph_matrix_to_tensor.py new file mode 100644 index 0000000000..2cdef20ed8 --- /dev/null +++ b/src/beignet/_graph_matrix_to_tensor.py @@ -0,0 +1,66 @@ +import numpy +import scipy +import scipy.sparse +from scipy.sparse import csr_matrix + +def _populate_graph(data, indices, indptr, graph, null_value): + """ + Populate the dense graph matrix from CSR sparse matrix attributes. + + Parameters: + - data: 1D numpy array of the non-zero values in the CSR matrix. + - indices: 1D numpy array of column indices corresponding to data. + - indptr: 1D numpy array of index pointers for the CSR matrix. + - graph: 2D numpy array (N x N) initialized with infinities. + - null_value: The value to assign to null entries in the graph. + + The function fills the graph with the minimum edge weights from the CSR matrix, + and assigns null_value to positions where there are no edges. + """ + N = graph.shape[0] + null_flag = numpy.ones((N, N), dtype=bool, order='C') + + # Calculate the number of non-zero entries per row + row_counts = indptr[1:] - indptr[:-1] + + # Generate row indices for all non-zero entries + rows = numpy.repeat(numpy.arange(N), row_counts) + + # Update null_flag to mark positions that have edges + null_flag[rows, indices] = False + + # Update the graph with the minimum values for each edge + graph[rows, indices] = numpy.minimum(data, graph[rows, indices]) + + # Assign null_value to positions with no edges + graph[null_flag] = null_value + +def graph_matrix_to_tensor(input: csr_matrix, null_value: float = 0) -> numpy.ndarray: + # Allow only csr, lil and csc matrices: other formats when converted to csr + # combine duplicated edges: we don't want this to happen in the background. + if not scipy.sparse.issparse(input): + raise ValueError + + if input.format not in {"lil", "csc", "csr"}: + raise ValueError + + input = input.tocsr() + + n = input.shape[0] + + if input.shape[1] != n: + raise ValueError + + data = numpy.asarray(input.data, dtype=numpy.float64, order="C") + + indices = numpy.asarray(input.indices, dtype=numpy.int32, order="C") + + indptr = numpy.asarray(input.indptr, dtype=numpy.int32, order="C") + + output = numpy.empty(input.shape, dtype=numpy.float64) + + output.fill(numpy.inf) + + _populate_graph(data, indices, indptr, output, null_value) + + return output diff --git a/src/beignet/_masked_tensor_to_graph_matrix.py b/src/beignet/_masked_tensor_to_graph_matrix.py new file mode 100644 index 0000000000..70957b78c0 --- /dev/null +++ b/src/beignet/_masked_tensor_to_graph_matrix.py @@ -0,0 +1,33 @@ +import numpy +from numpy.ma import MaskedArray +from scipy.sparse import csr_matrix + + +def masked_tensor_to_graph_matrix(input: MaskedArray) -> csr_matrix: + input = numpy.ma.asarray(input) + + if input.ndim != 2: + raise ValueError + + n = input.shape[0] + + if input.shape[1] != n: + raise ValueError + + compressed = input.compressed() + + mask = ~input.mask + + compressed = numpy.asarray(compressed, dtype=numpy.int32, order="c") + + idx_grid = numpy.empty((n, n), dtype=numpy.int32) + + idx_grid[:] = numpy.arange(n, dtype=numpy.int32) + + indices = numpy.asarray(idx_grid[mask], dtype=numpy.int32, order="c") + + indptr = numpy.zeros(n + 1, dtype=numpy.int32) + + indptr[1:] = mask.sum(1).cumsum() + + return csr_matrix((compressed, indices, indptr), (n, n)) diff --git a/src/beignet/_predecessor_matrix_to_distance_matrix.py b/src/beignet/_predecessor_matrix_to_distance_matrix.py new file mode 100644 index 0000000000..7b5edd4d55 --- /dev/null +++ b/src/beignet/_predecessor_matrix_to_distance_matrix.py @@ -0,0 +1,79 @@ +import numpy +import torch +from scipy.sparse import csr_matrix + +from ._validate_graph_matrix import validate_graph_matrix + +NULL_IDX = -9999 + +def _predecessor_matrix_to_distance_matrix( + input: numpy.ndarray, + predecessor_matrix: numpy.ndarray, + distance_matrix: numpy.ndarray, + directed: bool, + null_value: float, +): + n = input.shape[0] + + # symmetrize matrix, if necessary + if not directed: + input[input == 0] = numpy.inf + + for i in range(n): + for j in range(i + 1, n): + if input[j, i] <= input[i, j]: + input[i, j] = input[j, i] + else: + input[j, i] = input[i, j] + + for i in range(n): + for j in range(n): + null_path = True + + k2 = j + + while k2 != i: + k1 = predecessor_matrix[i, k2] + + if k1 == NULL_IDX: + break + + distance_matrix[i, j] += input[k1, k2] + + null_path = False + + k2 = k1 + + if null_path and i != j: + distance_matrix[i, j] = null_value + +def predecessor_matrix_to_distance_matrix( + input: numpy.ndarray | csr_matrix, + predecessor_matrix: numpy.ndarray, + directed: bool = True, + null_value: float = numpy.inf, +) -> numpy.ndarray: + input = validate_graph_matrix( + input, + directed, + dtype=torch.float64, + csr_output=False, + copy_if_dense=not directed, + ) + + predecessor_matrix = numpy.asarray(predecessor_matrix) + + if predecessor_matrix.shape != input.shape: + raise ValueError + + distance_matrix = numpy.zeros(input.shape, dtype=numpy.float64) + + _predecessor_matrix_to_distance_matrix( + input, + predecessor_matrix, + distance_matrix, + directed, + null_value, + ) + + return distance_matrix diff --git a/src/beignet/_reconstruct_path.py b/src/beignet/_reconstruct_path.py new file mode 100644 index 0000000000..9465d08ce9 --- /dev/null +++ b/src/beignet/_reconstruct_path.py @@ -0,0 +1,47 @@ +import numpy +import scipy +import scipy.sparse +from scipy.sparse import csr_matrix + +from ._validate_graph_matrix import validate_graph_matrix + + +def reconstruct_path( + input: numpy.ndarray | csr_matrix, + predecessors: numpy.ndarray, + directed: bool = True, +) -> csr_matrix: + input = validate_graph_matrix(input, directed, dense_output=False) + + n = input.shape[0] + + nnull = (predecessors < 0).sum() + + indices = numpy.argsort(predecessors)[nnull:].astype(numpy.int32) + + pind = predecessors[indices] + + indptr = pind.searchsorted(numpy.arange(n + 1)).astype(numpy.int32) + + data = input[pind, indices] + + if scipy.sparse.issparse(data): + data = data.todense() + + data = data.getA1() + + if not directed: + data2 = input[indices, pind] + + if scipy.sparse.issparse(data2): + data2 = data2.todense() + + data2 = data2.getA1() + + data[data == 0] = numpy.inf + + data2[data2 == 0] = numpy.inf + + data = numpy.minimum(data, data2) + + return csr_matrix((data, indices, indptr), shape=(n, n)) diff --git a/src/beignet/_tensor_to_graph_matrix.py b/src/beignet/_tensor_to_graph_matrix.py new file mode 100644 index 0000000000..6675b99629 --- /dev/null +++ b/src/beignet/_tensor_to_graph_matrix.py @@ -0,0 +1,21 @@ +import numpy +from scipy.sparse import csr_matrix + +from ._tensor_to_masked_graph_matrix import tensor_to_masked_graph_matrix +from ._masked_tensor_to_graph_matrix import masked_tensor_to_graph_matrix + + +def tensor_to_graph_matrix( + input: numpy.ndarray, + null_value: float = 0.0, + nan_is_null_edge: bool = True, + infinity_is_null_edge: bool = True, +) -> csr_matrix: + output = tensor_to_masked_graph_matrix( + input, + null_value, + nan_is_null_edge, + infinity_is_null_edge, + ) + + return masked_tensor_to_graph_matrix(output) diff --git a/src/beignet/_tensor_to_masked_graph_matrix.py b/src/beignet/_tensor_to_masked_graph_matrix.py new file mode 100644 index 0000000000..6ef8e5f741 --- /dev/null +++ b/src/beignet/_tensor_to_masked_graph_matrix.py @@ -0,0 +1,47 @@ +import numpy +from numpy.ma import MaskedArray + + +def tensor_to_masked_graph_matrix( + input: numpy.ndarray, + null_value: float = 0.0, + nan_null: bool = True, + infinity_null: bool = True, + copy: bool = True, +) -> MaskedArray: + input = numpy.array(input, copy=copy) + + if input.ndim != 2: + raise ValueError + + n = input.shape[0] + + if input.shape[1] != n: + raise ValueError + + if null_value is not None: + null_value = numpy.float64(null_value) + + if numpy.isnan(null_value): + nan_null = True + + null_value = None + elif numpy.isinf(null_value): + infinity_null = True + + null_value = None + + if null_value is None: + mask = numpy.zeros(input.shape, dtype="bool") + + input = numpy.ma.masked_array(input, mask, copy=False) + else: + input = numpy.ma.masked_values(input, null_value, copy=False) + + if infinity_null: + input.mask |= numpy.isinf(input) + + if nan_null: + input.mask |= numpy.isnan(input) + + return input diff --git a/src/beignet/_validate_graph_matrix.py b/src/beignet/_validate_graph_matrix.py new file mode 100644 index 0000000000..e6ceba3cff --- /dev/null +++ b/src/beignet/_validate_graph_matrix.py @@ -0,0 +1,73 @@ +import numpy +import scipy.sparse +import torch + +from ._tensor_to_graph_matrix import tensor_to_graph_matrix +from ._tensor_to_masked_graph_matrix import tensor_to_masked_graph_matrix +from ._graph_matrix_to_tensor import graph_matrix_to_tensor +from ._masked_tensor_to_graph_matrix import masked_tensor_to_graph_matrix + + +def validate_graph_matrix( + graph: numpy.ndarray, + directed: bool, + csr_output=True, + dense_output=True, + copy_if_dense=False, + copy_if_sparse=False, + null_value_in=0, + null_value_out=numpy.inf, + infinity_null=True, + nan_null=True, + dtype=torch.float64, +): + """Routine for validation and conversion of csgraph inputs""" + if not (csr_output or dense_output): + raise ValueError("Internal: dense or csr output must be true") + + accept_fv = [null_value_in] + + if infinity_null: + accept_fv.append(numpy.inf) + + if nan_null: + accept_fv.append(numpy.nan) + + # if undirected and csc storage, then transposing in-place + # is quicker than later converting to csr. + if (not directed) and scipy.sparse.issparse(graph) and graph.format == "csc": + graph = graph.T + + if scipy.sparse.issparse(graph): + if csr_output: + graph = scipy.sparse.csr_matrix(graph, dtype=dtype, copy=copy_if_sparse) + else: + graph = graph_matrix_to_tensor(graph, null_value=null_value_out) + elif numpy.ma.isMaskedArray(graph): + if dense_output: + mask = graph.mask + + graph = numpy.array(graph.data, dtype=dtype, copy=copy_if_dense) + + graph[mask] = null_value_out + else: + graph = masked_tensor_to_graph_matrix(graph) + else: + if dense_output: + graph = tensor_to_masked_graph_matrix(graph, copy=copy_if_dense, null_value=null_value_in, nan_null=nan_null, infinity_null=infinity_null) + + mask = graph.mask + + graph = numpy.asarray(graph.data, dtype=dtype) + + graph[mask] = null_value_out + else: + graph = tensor_to_graph_matrix(graph, null_value=null_value_in, infinity_is_null_edge=infinity_null, nan_is_null_edge=nan_null) + + if graph.ndim != 2: + raise ValueError("compressed-sparse graph must be 2-D") + + if graph.shape[0] != graph.shape[1]: + raise ValueError("compressed-sparse graph must be shape (N, N)") + + return graph diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/beignet/test__graph_matrix_to_masked_tensor.py b/tests/beignet/test__graph_matrix_to_masked_tensor.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/beignet/test__graph_matrix_to_tensor.py b/tests/beignet/test__graph_matrix_to_tensor.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/beignet/test__masked_tensor_to_graph_matrix.py b/tests/beignet/test__masked_tensor_to_graph_matrix.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/beignet/test__predecessor_matrix_to_distance_matrix.py b/tests/beignet/test__predecessor_matrix_to_distance_matrix.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/beignet/test__reconstruct_path.py b/tests/beignet/test__reconstruct_path.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/beignet/test__tensor_to_graph_matrix.py b/tests/beignet/test__tensor_to_graph_matrix.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/beignet/test__tensor_to_masked_graph_matrix.py b/tests/beignet/test__tensor_to_masked_graph_matrix.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/beignet/test__validate_graph_matrix.py b/tests/beignet/test__validate_graph_matrix.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/csgraph/__init__.py b/tests/csgraph/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/csgraph/meson.build b/tests/csgraph/meson.build new file mode 100644 index 0000000000..7ad02ba3b2 --- /dev/null +++ b/tests/csgraph/meson.build @@ -0,0 +1,20 @@ +python_sources = [ + '__init__.py', + 'test_connected_components.py', + 'test_conversions.py', + 'test_flow.py', + 'test_graph_laplacian.py', + 'test_matching.py', + 'test_pydata_sparse.py', + 'test_reordering.py', + 'test_shortest_path.py', + 'test_spanning_tree.py', + 'test_traversal.py' +] + + +py3.install_sources( + python_sources, + subdir: 'scipy/sparse/csgraph/tests', + install_tag: 'tests' +) diff --git a/tests/csgraph/test_connected_components.py b/tests/csgraph/test_connected_components.py new file mode 100644 index 0000000000..0b190a24de --- /dev/null +++ b/tests/csgraph/test_connected_components.py @@ -0,0 +1,119 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_almost_equal +from scipy.sparse import csgraph, csr_array + + +def test_weak_connections(): + Xde = np.array([[0, 1, 0], + [0, 0, 0], + [0, 0, 0]]) + + Xsp = csgraph.csgraph_from_dense(Xde, null_value=0) + + for X in Xsp, Xde: + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='weak') + + assert_equal(n_components, 2) + assert_array_almost_equal(labels, [0, 0, 1]) + + +def test_strong_connections(): + X1de = np.array([[0, 1, 0], + [0, 0, 0], + [0, 0, 0]]) + X2de = X1de + X1de.T + + X1sp = csgraph.csgraph_from_dense(X1de, null_value=0) + X2sp = csgraph.csgraph_from_dense(X2de, null_value=0) + + for X in X1sp, X1de: + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='strong') + + assert_equal(n_components, 3) + labels.sort() + assert_array_almost_equal(labels, [0, 1, 2]) + + for X in X2sp, X2de: + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='strong') + + assert_equal(n_components, 2) + labels.sort() + assert_array_almost_equal(labels, [0, 0, 1]) + + +def test_strong_connections2(): + X = np.array([[0, 0, 0, 0, 0, 0], + [1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0]]) + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='strong') + assert_equal(n_components, 5) + labels.sort() + assert_array_almost_equal(labels, [0, 1, 2, 2, 3, 4]) + + +def test_weak_connections2(): + X = np.array([[0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0]]) + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='weak') + assert_equal(n_components, 2) + labels.sort() + assert_array_almost_equal(labels, [0, 0, 1, 1, 1, 1]) + + +def test_ticket1876(): + # Regression test: this failed in the original implementation + # There should be two strongly-connected components; previously gave one + g = np.array([[0, 1, 1, 0], + [1, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 1, 0]]) + n_components, labels = csgraph.connected_components(g, connection='strong') + + assert_equal(n_components, 2) + assert_equal(labels[0], labels[1]) + assert_equal(labels[2], labels[3]) + + +def test_fully_connected_graph(): + # Fully connected dense matrices raised an exception. + # https://github.com/scipy/scipy/issues/3818 + g = np.ones((4, 4)) + n_components, labels = csgraph.connected_components(g) + assert_equal(n_components, 1) + + +def test_int64_indices_undirected(): + # See https://github.com/scipy/scipy/issues/18716 + g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2)) + assert g.indices.dtype == np.int64 + n, labels = csgraph.connected_components(g, directed=False) + assert n == 1 + assert_array_almost_equal(labels, [0, 0]) + + +def test_int64_indices_directed(): + # See https://github.com/scipy/scipy/issues/18716 + g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2)) + assert g.indices.dtype == np.int64 + n, labels = csgraph.connected_components(g, directed=True, + connection='strong') + assert n == 2 + assert_array_almost_equal(labels, [1, 0]) + diff --git a/tests/csgraph/test_conversions.py b/tests/csgraph/test_conversions.py new file mode 100644 index 0000000000..e7900d67b5 --- /dev/null +++ b/tests/csgraph/test_conversions.py @@ -0,0 +1,61 @@ +import numpy as np +from numpy.testing import assert_array_almost_equal +from scipy.sparse import csr_matrix +from scipy.sparse.csgraph import csgraph_from_dense, csgraph_to_dense + + +def test_csgraph_from_dense(): + np.random.seed(1234) + G = np.random.random((10, 10)) + some_nulls = (G < 0.4) + all_nulls = (G < 0.8) + + for null_value in [0, np.nan, np.inf]: + G[all_nulls] = null_value + with np.errstate(invalid="ignore"): + G_csr = csgraph_from_dense(G, null_value=0) + + G[all_nulls] = 0 + assert_array_almost_equal(G, G_csr.toarray()) + + for null_value in [np.nan, np.inf]: + G[all_nulls] = 0 + G[some_nulls] = null_value + with np.errstate(invalid="ignore"): + G_csr = csgraph_from_dense(G, null_value=0) + + G[all_nulls] = 0 + assert_array_almost_equal(G, G_csr.toarray()) + + +def test_csgraph_to_dense(): + np.random.seed(1234) + G = np.random.random((10, 10)) + nulls = (G < 0.8) + G[nulls] = np.inf + + G_csr = csgraph_from_dense(G) + + for null_value in [0, 10, -np.inf, np.inf]: + G[nulls] = null_value + assert_array_almost_equal(G, csgraph_to_dense(G_csr, null_value)) + + +def test_multiple_edges(): + # create a random square matrix with an even number of elements + np.random.seed(1234) + X = np.random.random((10, 10)) + Xcsr = csr_matrix(X) + + # now double-up every other column + Xcsr.indices[::2] = Xcsr.indices[1::2] + + # normal sparse toarray() will sum the duplicated edges + Xdense = Xcsr.toarray() + assert_array_almost_equal(Xdense[:, 1::2], + X[:, ::2] + X[:, 1::2]) + + # csgraph_to_dense chooses the minimum of each duplicated edge + Xdense = csgraph_to_dense(Xcsr) + assert_array_almost_equal(Xdense[:, 1::2], + np.minimum(X[:, ::2], X[:, 1::2])) diff --git a/tests/csgraph/test_flow.py b/tests/csgraph/test_flow.py new file mode 100644 index 0000000000..8bb129a572 --- /dev/null +++ b/tests/csgraph/test_flow.py @@ -0,0 +1,201 @@ +import numpy as np +from numpy.testing import assert_array_equal +import pytest + +from scipy.sparse import csr_matrix, csc_matrix +from scipy.sparse.csgraph import maximum_flow +from scipy.sparse.csgraph._flow import ( + _add_reverse_edges, _make_edge_pointers, _make_tails +) + +methods = ['edmonds_karp', 'dinic'] + +def test_raises_on_dense_input(): + with pytest.raises(TypeError): + graph = np.array([[0, 1], [0, 0]]) + maximum_flow(graph, 0, 1) + maximum_flow(graph, 0, 1, method='edmonds_karp') + + +def test_raises_on_csc_input(): + with pytest.raises(TypeError): + graph = csc_matrix([[0, 1], [0, 0]]) + maximum_flow(graph, 0, 1) + maximum_flow(graph, 0, 1, method='edmonds_karp') + + +def test_raises_on_floating_point_input(): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1.5], [0, 0]], dtype=np.float64) + maximum_flow(graph, 0, 1) + maximum_flow(graph, 0, 1, method='edmonds_karp') + + +def test_raises_on_non_square_input(): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1, 2], [2, 1, 0]]) + maximum_flow(graph, 0, 1) + + +def test_raises_when_source_is_sink(): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1], [0, 0]]) + maximum_flow(graph, 0, 0) + maximum_flow(graph, 0, 0, method='edmonds_karp') + + +@pytest.mark.parametrize('method', methods) +@pytest.mark.parametrize('source', [-1, 2, 3]) +def test_raises_when_source_is_out_of_bounds(source, method): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1], [0, 0]]) + maximum_flow(graph, source, 1, method=method) + + +@pytest.mark.parametrize('method', methods) +@pytest.mark.parametrize('sink', [-1, 2, 3]) +def test_raises_when_sink_is_out_of_bounds(sink, method): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1], [0, 0]]) + maximum_flow(graph, 0, sink, method=method) + + +@pytest.mark.parametrize('method', methods) +def test_simple_graph(method): + # This graph looks as follows: + # (0) --5--> (1) + graph = csr_matrix([[0, 5], [0, 0]]) + res = maximum_flow(graph, 0, 1, method=method) + assert res.flow_value == 5 + expected_flow = np.array([[0, 5], [-5, 0]]) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_bottle_neck_graph(method): + # This graph cannot use the full capacity between 0 and 1: + # (0) --5--> (1) --3--> (2) + graph = csr_matrix([[0, 5, 0], [0, 0, 3], [0, 0, 0]]) + res = maximum_flow(graph, 0, 2, method=method) + assert res.flow_value == 3 + expected_flow = np.array([[0, 3, 0], [-3, 0, 3], [0, -3, 0]]) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_backwards_flow(method): + # This example causes backwards flow between vertices 3 and 4, + # and so this test ensures that we handle that accordingly. See + # https://stackoverflow.com/q/38843963/5085211 + # for more information. + graph = csr_matrix([[0, 10, 0, 0, 10, 0, 0, 0], + [0, 0, 10, 0, 0, 0, 0, 0], + [0, 0, 0, 10, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 10], + [0, 0, 0, 10, 0, 10, 0, 0], + [0, 0, 0, 0, 0, 0, 10, 0], + [0, 0, 0, 0, 0, 0, 0, 10], + [0, 0, 0, 0, 0, 0, 0, 0]]) + res = maximum_flow(graph, 0, 7, method=method) + assert res.flow_value == 20 + expected_flow = np.array([[0, 10, 0, 0, 10, 0, 0, 0], + [-10, 0, 10, 0, 0, 0, 0, 0], + [0, -10, 0, 10, 0, 0, 0, 0], + [0, 0, -10, 0, 0, 0, 0, 10], + [-10, 0, 0, 0, 0, 10, 0, 0], + [0, 0, 0, 0, -10, 0, 10, 0], + [0, 0, 0, 0, 0, -10, 0, 10], + [0, 0, 0, -10, 0, 0, -10, 0]]) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_example_from_clrs_chapter_26_1(method): + # See page 659 in CLRS second edition, but note that the maximum flow + # we find is slightly different than the one in CLRS; we push a flow of + # 12 to v_1 instead of v_2. + graph = csr_matrix([[0, 16, 13, 0, 0, 0], + [0, 0, 10, 12, 0, 0], + [0, 4, 0, 0, 14, 0], + [0, 0, 9, 0, 0, 20], + [0, 0, 0, 7, 0, 4], + [0, 0, 0, 0, 0, 0]]) + res = maximum_flow(graph, 0, 5, method=method) + assert res.flow_value == 23 + expected_flow = np.array([[0, 12, 11, 0, 0, 0], + [-12, 0, 0, 12, 0, 0], + [-11, 0, 0, 0, 11, 0], + [0, -12, 0, 0, -7, 19], + [0, 0, -11, 7, 0, 4], + [0, 0, 0, -19, -4, 0]]) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_disconnected_graph(method): + # This tests the following disconnected graph: + # (0) --5--> (1) (2) --3--> (3) + graph = csr_matrix([[0, 5, 0, 0], + [0, 0, 0, 0], + [0, 0, 9, 3], + [0, 0, 0, 0]]) + res = maximum_flow(graph, 0, 3, method=method) + assert res.flow_value == 0 + expected_flow = np.zeros((4, 4), dtype=np.int32) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_add_reverse_edges_large_graph(method): + # Regression test for https://github.com/scipy/scipy/issues/14385 + n = 100_000 + indices = np.arange(1, n) + indptr = np.array(list(range(n)) + [n - 1]) + data = np.ones(n - 1, dtype=np.int32) + graph = csr_matrix((data, indices, indptr), shape=(n, n)) + res = maximum_flow(graph, 0, n - 1, method=method) + assert res.flow_value == 1 + expected_flow = graph - graph.transpose() + assert_array_equal(res.flow.data, expected_flow.data) + assert_array_equal(res.flow.indices, expected_flow.indices) + assert_array_equal(res.flow.indptr, expected_flow.indptr) + + +@pytest.mark.parametrize("a,b_data_expected", [ + ([[]], []), + ([[0], [0]], []), + ([[1, 0, 2], [0, 0, 0], [0, 3, 0]], [1, 2, 0, 0, 3]), + ([[9, 8, 7], [4, 5, 6], [0, 0, 0]], [9, 8, 7, 4, 5, 6, 0, 0])]) +def test_add_reverse_edges(a, b_data_expected): + """Test that the reversal of the edges of the input graph works + as expected. + """ + a = csr_matrix(a, dtype=np.int32, shape=(len(a), len(a))) + b = _add_reverse_edges(a) + assert_array_equal(b.data, b_data_expected) + + +@pytest.mark.parametrize("a,expected", [ + ([[]], []), + ([[0]], []), + ([[1]], [0]), + ([[0, 1], [10, 0]], [1, 0]), + ([[1, 0, 2], [0, 0, 3], [4, 5, 0]], [0, 3, 4, 1, 2]) +]) +def test_make_edge_pointers(a, expected): + a = csr_matrix(a, dtype=np.int32) + rev_edge_ptr = _make_edge_pointers(a) + assert_array_equal(rev_edge_ptr, expected) + + +@pytest.mark.parametrize("a,expected", [ + ([[]], []), + ([[0]], []), + ([[1]], [0]), + ([[0, 1], [10, 0]], [0, 1]), + ([[1, 0, 2], [0, 0, 3], [4, 5, 0]], [0, 0, 1, 2, 2]) +]) +def test_make_tails(a, expected): + a = csr_matrix(a, dtype=np.int32) + tails = _make_tails(a) + assert_array_equal(tails, expected) diff --git a/tests/csgraph/test_graph_laplacian.py b/tests/csgraph/test_graph_laplacian.py new file mode 100644 index 0000000000..88805db0c9 --- /dev/null +++ b/tests/csgraph/test_graph_laplacian.py @@ -0,0 +1,368 @@ +import pytest +import numpy as np +from numpy.testing import assert_allclose +from pytest import raises as assert_raises +from scipy import sparse + +from scipy.sparse import csgraph +from scipy._lib._util import np_long, np_ulong + + +def check_int_type(mat): + return np.issubdtype(mat.dtype, np.signedinteger) or np.issubdtype( + mat.dtype, np_ulong + ) + + +def test_laplacian_value_error(): + for t in int, float, complex: + for m in ([1, 1], + [[[1]]], + [[1, 2, 3], [4, 5, 6]], + [[1, 2], [3, 4], [5, 5]]): + A = np.array(m, dtype=t) + assert_raises(ValueError, csgraph.laplacian, A) + + +def _explicit_laplacian(x, normed=False): + if sparse.issparse(x): + x = x.toarray() + x = np.asarray(x) + y = -1.0 * x + for j in range(y.shape[0]): + y[j,j] = x[j,j+1:].sum() + x[j,:j].sum() + if normed: + d = np.diag(y).copy() + d[d == 0] = 1.0 + y /= d[:,None]**.5 + y /= d[None,:]**.5 + return y + + +def _check_symmetric_graph_laplacian(mat, normed, copy=True): + if not hasattr(mat, 'shape'): + mat = eval(mat, dict(np=np, sparse=sparse)) + + if sparse.issparse(mat): + sp_mat = mat + mat = sp_mat.toarray() + else: + sp_mat = sparse.csr_matrix(mat) + + mat_copy = np.copy(mat) + sp_mat_copy = sparse.csr_matrix(sp_mat, copy=True) + + n_nodes = mat.shape[0] + explicit_laplacian = _explicit_laplacian(mat, normed=normed) + laplacian = csgraph.laplacian(mat, normed=normed, copy=copy) + sp_laplacian = csgraph.laplacian(sp_mat, normed=normed, + copy=copy) + + if copy: + assert_allclose(mat, mat_copy) + _assert_allclose_sparse(sp_mat, sp_mat_copy) + else: + if not (normed and check_int_type(mat)): + assert_allclose(laplacian, mat) + if sp_mat.format == 'coo': + _assert_allclose_sparse(sp_laplacian, sp_mat) + + assert_allclose(laplacian, sp_laplacian.toarray()) + + for tested in [laplacian, sp_laplacian.toarray()]: + if not normed: + assert_allclose(tested.sum(axis=0), np.zeros(n_nodes)) + assert_allclose(tested.T, tested) + assert_allclose(tested, explicit_laplacian) + + +def test_symmetric_graph_laplacian(): + symmetric_mats = ( + 'np.arange(10) * np.arange(10)[:, np.newaxis]', + 'np.ones((7, 7))', + 'np.eye(19)', + 'sparse.diags([1, 1], [-1, 1], shape=(4, 4))', + 'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).toarray()', + 'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).todense()', + 'np.vander(np.arange(4)) + np.vander(np.arange(4)).T' + ) + for mat in symmetric_mats: + for normed in True, False: + for copy in True, False: + _check_symmetric_graph_laplacian(mat, normed, copy) + + +def _assert_allclose_sparse(a, b, **kwargs): + # helper function that can deal with sparse matrices + if sparse.issparse(a): + a = a.toarray() + if sparse.issparse(b): + b = b.toarray() + assert_allclose(a, b, **kwargs) + + +def _check_laplacian_dtype_none( + A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type +): + mat = arr_type(A, dtype=dtype) + L, d = csgraph.laplacian( + mat, + normed=normed, + return_diag=True, + use_out_degree=use_out_degree, + copy=copy, + dtype=None, + ) + if normed and check_int_type(mat): + assert L.dtype == np.float64 + assert d.dtype == np.float64 + _assert_allclose_sparse(L, desired_L, atol=1e-12) + _assert_allclose_sparse(d, desired_d, atol=1e-12) + else: + assert L.dtype == dtype + assert d.dtype == dtype + desired_L = np.asarray(desired_L).astype(dtype) + desired_d = np.asarray(desired_d).astype(dtype) + _assert_allclose_sparse(L, desired_L, atol=1e-12) + _assert_allclose_sparse(d, desired_d, atol=1e-12) + + if not copy: + if not (normed and check_int_type(mat)): + if type(mat) is np.ndarray: + assert_allclose(L, mat) + elif mat.format == "coo": + _assert_allclose_sparse(L, mat) + + +def _check_laplacian_dtype( + A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type +): + mat = arr_type(A, dtype=dtype) + L, d = csgraph.laplacian( + mat, + normed=normed, + return_diag=True, + use_out_degree=use_out_degree, + copy=copy, + dtype=dtype, + ) + assert L.dtype == dtype + assert d.dtype == dtype + desired_L = np.asarray(desired_L).astype(dtype) + desired_d = np.asarray(desired_d).astype(dtype) + _assert_allclose_sparse(L, desired_L, atol=1e-12) + _assert_allclose_sparse(d, desired_d, atol=1e-12) + + if not copy: + if not (normed and check_int_type(mat)): + if type(mat) is np.ndarray: + assert_allclose(L, mat) + elif mat.format == 'coo': + _assert_allclose_sparse(L, mat) + + +INT_DTYPES = (np.intc, np_long, np.longlong) +REAL_DTYPES = (np.float32, np.float64, np.longdouble) +COMPLEX_DTYPES = (np.complex64, np.complex128, np.clongdouble) +DTYPES = INT_DTYPES + REAL_DTYPES + COMPLEX_DTYPES + + +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("arr_type", [np.array, + sparse.csr_matrix, + sparse.coo_matrix, + sparse.csr_array, + sparse.coo_array]) +@pytest.mark.parametrize("copy", [True, False]) +@pytest.mark.parametrize("normed", [True, False]) +@pytest.mark.parametrize("use_out_degree", [True, False]) +def test_asymmetric_laplacian(use_out_degree, normed, + copy, dtype, arr_type): + # adjacency matrix + A = [[0, 1, 0], + [4, 2, 0], + [0, 0, 0]] + A = arr_type(np.array(A), dtype=dtype) + A_copy = A.copy() + + if not normed and use_out_degree: + # Laplacian matrix using out-degree + L = [[1, -1, 0], + [-4, 4, 0], + [0, 0, 0]] + d = [1, 4, 0] + + if normed and use_out_degree: + # normalized Laplacian matrix using out-degree + L = [[1, -0.5, 0], + [-2, 1, 0], + [0, 0, 0]] + d = [1, 2, 1] + + if not normed and not use_out_degree: + # Laplacian matrix using in-degree + L = [[4, -1, 0], + [-4, 1, 0], + [0, 0, 0]] + d = [4, 1, 0] + + if normed and not use_out_degree: + # normalized Laplacian matrix using in-degree + L = [[1, -0.5, 0], + [-2, 1, 0], + [0, 0, 0]] + d = [2, 1, 1] + + _check_laplacian_dtype_none( + A, + L, + d, + normed=normed, + use_out_degree=use_out_degree, + copy=copy, + dtype=dtype, + arr_type=arr_type, + ) + + _check_laplacian_dtype( + A_copy, + L, + d, + normed=normed, + use_out_degree=use_out_degree, + copy=copy, + dtype=dtype, + arr_type=arr_type, + ) + + +@pytest.mark.parametrize("fmt", ['csr', 'csc', 'coo', 'lil', + 'dok', 'dia', 'bsr']) +@pytest.mark.parametrize("normed", [True, False]) +@pytest.mark.parametrize("copy", [True, False]) +def test_sparse_formats(fmt, normed, copy): + mat = sparse.diags([1, 1], [-1, 1], shape=(4, 4), format=fmt) + _check_symmetric_graph_laplacian(mat, normed, copy) + + +@pytest.mark.parametrize( + "arr_type", [np.asarray, + sparse.csr_matrix, + sparse.coo_matrix, + sparse.csr_array, + sparse.coo_array] +) +@pytest.mark.parametrize("form", ["array", "function", "lo"]) +def test_laplacian_symmetrized(arr_type, form): + # adjacency matrix + n = 3 + mat = arr_type(np.arange(n * n).reshape(n, n)) + L_in, d_in = csgraph.laplacian( + mat, + return_diag=True, + form=form, + ) + L_out, d_out = csgraph.laplacian( + mat, + return_diag=True, + use_out_degree=True, + form=form, + ) + Ls, ds = csgraph.laplacian( + mat, + return_diag=True, + symmetrized=True, + form=form, + ) + Ls_normed, ds_normed = csgraph.laplacian( + mat, + return_diag=True, + symmetrized=True, + normed=True, + form=form, + ) + mat += mat.T + Lss, dss = csgraph.laplacian(mat, return_diag=True, form=form) + Lss_normed, dss_normed = csgraph.laplacian( + mat, + return_diag=True, + normed=True, + form=form, + ) + + assert_allclose(ds, d_in + d_out) + assert_allclose(ds, dss) + assert_allclose(ds_normed, dss_normed) + + d = {} + for L in ["L_in", "L_out", "Ls", "Ls_normed", "Lss", "Lss_normed"]: + if form == "array": + d[L] = eval(L) + else: + d[L] = eval(L)(np.eye(n, dtype=mat.dtype)) + + _assert_allclose_sparse(d["Ls"], d["L_in"] + d["L_out"].T) + _assert_allclose_sparse(d["Ls"], d["Lss"]) + _assert_allclose_sparse(d["Ls_normed"], d["Lss_normed"]) + + +@pytest.mark.parametrize( + "arr_type", [np.asarray, + sparse.csr_matrix, + sparse.coo_matrix, + sparse.csr_array, + sparse.coo_array] +) +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("normed", [True, False]) +@pytest.mark.parametrize("symmetrized", [True, False]) +@pytest.mark.parametrize("use_out_degree", [True, False]) +@pytest.mark.parametrize("form", ["function", "lo"]) +def test_format(dtype, arr_type, normed, symmetrized, use_out_degree, form): + n = 3 + mat = [[0, 1, 0], [4, 2, 0], [0, 0, 0]] + mat = arr_type(np.array(mat), dtype=dtype) + Lo, do = csgraph.laplacian( + mat, + return_diag=True, + normed=normed, + symmetrized=symmetrized, + use_out_degree=use_out_degree, + dtype=dtype, + ) + La, da = csgraph.laplacian( + mat, + return_diag=True, + normed=normed, + symmetrized=symmetrized, + use_out_degree=use_out_degree, + dtype=dtype, + form="array", + ) + assert_allclose(do, da) + _assert_allclose_sparse(Lo, La) + + L, d = csgraph.laplacian( + mat, + return_diag=True, + normed=normed, + symmetrized=symmetrized, + use_out_degree=use_out_degree, + dtype=dtype, + form=form, + ) + assert_allclose(d, do) + assert d.dtype == dtype + Lm = L(np.eye(n, dtype=mat.dtype)).astype(dtype) + _assert_allclose_sparse(Lm, Lo, rtol=2e-7, atol=2e-7) + x = np.arange(6).reshape(3, 2) + if not (normed and dtype in INT_DTYPES): + assert_allclose(L(x), Lo @ x) + else: + # Normalized Lo is casted to integer, but L() is not + pass + + +def test_format_error_message(): + with pytest.raises(ValueError, match="Invalid form: 'toto'"): + _ = csgraph.laplacian(np.eye(1), form='toto') diff --git a/tests/csgraph/test_matching.py b/tests/csgraph/test_matching.py new file mode 100644 index 0000000000..87e2920fe9 --- /dev/null +++ b/tests/csgraph/test_matching.py @@ -0,0 +1,294 @@ +from itertools import product + +import numpy as np +from numpy.testing import assert_array_equal, assert_equal +import pytest + +from scipy.sparse import csr_matrix, coo_matrix, diags +from scipy.sparse.csgraph import ( + maximum_bipartite_matching, min_weight_full_bipartite_matching +) + + +def test_maximum_bipartite_matching_raises_on_dense_input(): + with pytest.raises(TypeError): + graph = np.array([[0, 1], [0, 0]]) + maximum_bipartite_matching(graph) + + +def test_maximum_bipartite_matching_empty_graph(): + graph = csr_matrix((0, 0)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + expected_matching = np.array([]) + assert_array_equal(expected_matching, x) + assert_array_equal(expected_matching, y) + + +def test_maximum_bipartite_matching_empty_left_partition(): + graph = csr_matrix((2, 0)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + assert_array_equal(np.array([]), x) + assert_array_equal(np.array([-1, -1]), y) + + +def test_maximum_bipartite_matching_empty_right_partition(): + graph = csr_matrix((0, 3)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + assert_array_equal(np.array([-1, -1, -1]), x) + assert_array_equal(np.array([]), y) + + +def test_maximum_bipartite_matching_graph_with_no_edges(): + graph = csr_matrix((2, 2)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + assert_array_equal(np.array([-1, -1]), x) + assert_array_equal(np.array([-1, -1]), y) + + +def test_maximum_bipartite_matching_graph_that_causes_augmentation(): + # In this graph, column 1 is initially assigned to row 1, but it should be + # reassigned to make room for row 2. + graph = csr_matrix([[1, 1], [1, 0]]) + x = maximum_bipartite_matching(graph, perm_type='column') + y = maximum_bipartite_matching(graph, perm_type='row') + expected_matching = np.array([1, 0]) + assert_array_equal(expected_matching, x) + assert_array_equal(expected_matching, y) + + +def test_maximum_bipartite_matching_graph_with_more_rows_than_columns(): + graph = csr_matrix([[1, 1], [1, 0], [0, 1]]) + x = maximum_bipartite_matching(graph, perm_type='column') + y = maximum_bipartite_matching(graph, perm_type='row') + assert_array_equal(np.array([0, -1, 1]), x) + assert_array_equal(np.array([0, 2]), y) + + +def test_maximum_bipartite_matching_graph_with_more_columns_than_rows(): + graph = csr_matrix([[1, 1, 0], [0, 0, 1]]) + x = maximum_bipartite_matching(graph, perm_type='column') + y = maximum_bipartite_matching(graph, perm_type='row') + assert_array_equal(np.array([0, 2]), x) + assert_array_equal(np.array([0, -1, 1]), y) + + +def test_maximum_bipartite_matching_explicit_zeros_count_as_edges(): + data = [0, 0] + indices = [1, 0] + indptr = [0, 1, 2] + graph = csr_matrix((data, indices, indptr), shape=(2, 2)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + expected_matching = np.array([1, 0]) + assert_array_equal(expected_matching, x) + assert_array_equal(expected_matching, y) + + +def test_maximum_bipartite_matching_feasibility_of_result(): + # This is a regression test for GitHub issue #11458 + data = np.ones(50, dtype=int) + indices = [11, 12, 19, 22, 23, 5, 22, 3, 8, 10, 5, 6, 11, 12, 13, 5, 13, + 14, 20, 22, 3, 15, 3, 13, 14, 11, 12, 19, 22, 23, 5, 22, 3, 8, + 10, 5, 6, 11, 12, 13, 5, 13, 14, 20, 22, 3, 15, 3, 13, 14] + indptr = [0, 5, 7, 10, 10, 15, 20, 22, 22, 23, 25, 30, 32, 35, 35, 40, 45, + 47, 47, 48, 50] + graph = csr_matrix((data, indices, indptr), shape=(20, 25)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + assert (x != -1).sum() == 13 + assert (y != -1).sum() == 13 + # Ensure that each element of the matching is in fact an edge in the graph. + for u, v in zip(range(graph.shape[0]), y): + if v != -1: + assert graph[u, v] + for u, v in zip(x, range(graph.shape[1])): + if u != -1: + assert graph[u, v] + + +def test_matching_large_random_graph_with_one_edge_incident_to_each_vertex(): + np.random.seed(42) + A = diags(np.ones(25), offsets=0, format='csr') + rand_perm = np.random.permutation(25) + rand_perm2 = np.random.permutation(25) + + Rrow = np.arange(25) + Rcol = rand_perm + Rdata = np.ones(25, dtype=int) + Rmat = coo_matrix((Rdata, (Rrow, Rcol))).tocsr() + + Crow = rand_perm2 + Ccol = np.arange(25) + Cdata = np.ones(25, dtype=int) + Cmat = coo_matrix((Cdata, (Crow, Ccol))).tocsr() + # Randomly permute identity matrix + B = Rmat * A * Cmat + + # Row permute + perm = maximum_bipartite_matching(B, perm_type='row') + Rrow = np.arange(25) + Rcol = perm + Rdata = np.ones(25, dtype=int) + Rmat = coo_matrix((Rdata, (Rrow, Rcol))).tocsr() + C1 = Rmat * B + + # Column permute + perm2 = maximum_bipartite_matching(B, perm_type='column') + Crow = perm2 + Ccol = np.arange(25) + Cdata = np.ones(25, dtype=int) + Cmat = coo_matrix((Cdata, (Crow, Ccol))).tocsr() + C2 = B * Cmat + + # Should get identity matrix back + assert_equal(any(C1.diagonal() == 0), False) + assert_equal(any(C2.diagonal() == 0), False) + + +@pytest.mark.parametrize('num_rows,num_cols', [(0, 0), (2, 0), (0, 3)]) +def test_min_weight_full_matching_trivial_graph(num_rows, num_cols): + biadjacency_matrix = csr_matrix((num_cols, num_rows)) + row_ind, col_ind = min_weight_full_bipartite_matching(biadjacency_matrix) + assert len(row_ind) == 0 + assert len(col_ind) == 0 + + +@pytest.mark.parametrize('biadjacency_matrix', + [ + [[1, 1, 1], [1, 0, 0], [1, 0, 0]], + [[1, 1, 1], [0, 0, 1], [0, 0, 1]], + [[1, 0, 0, 1], [1, 1, 0, 1], [0, 0, 0, 0]], + [[1, 0, 0], [2, 0, 0]], + [[0, 1, 0], [0, 2, 0]], + [[1, 0], [2, 0], [5, 0]] + ]) +def test_min_weight_full_matching_infeasible_problems(biadjacency_matrix): + with pytest.raises(ValueError): + min_weight_full_bipartite_matching(csr_matrix(biadjacency_matrix)) + + +def test_min_weight_full_matching_large_infeasible(): + # Regression test for GitHub issue #17269 + a = np.asarray([ + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001], + [0.0, 0.11687445, 0.0, 0.0, 0.01319788, 0.07509257, 0.0, + 0.0, 0.0, 0.74228317, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.81087935, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.8408466, 0.0, 0.0, 0.0, 0.0, 0.01194389, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.82994211, 0.0, 0.0, 0.0, 0.11468516, 0.0, 0.0, 0.0, + 0.11173505, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0], + [0.18796507, 0.0, 0.04002318, 0.0, 0.0, 0.0, 0.0, 0.0, 0.75883335, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.71545464, 0.0, 0.0, 0.0, 0.0, 0.0, 0.02748488, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.78470564, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.14829198, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.10870609, 0.0, 0.0, 0.0, 0.8918677, 0.0, 0.0, 0.0, 0.06306644, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.63844085, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7442354, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09850549, 0.0, 0.0, 0.18638258, + 0.2769244, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.73182464, 0.0, 0.0, 0.46443561, + 0.38589284, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.29510278, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09666032, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + ]) + with pytest.raises(ValueError, match='no full matching exists'): + min_weight_full_bipartite_matching(csr_matrix(a)) + + +def test_explicit_zero_causes_warning(): + with pytest.warns(UserWarning): + biadjacency_matrix = csr_matrix(((2, 0, 3), (0, 1, 1), (0, 2, 3))) + min_weight_full_bipartite_matching(biadjacency_matrix) + + +# General test for linear sum assignment solvers to make it possible to rely +# on the same tests for scipy.optimize.linear_sum_assignment. +def linear_sum_assignment_assertions( + solver, array_type, sign, test_case +): + cost_matrix, expected_cost = test_case + maximize = sign == -1 + cost_matrix = sign * array_type(cost_matrix) + expected_cost = sign * np.array(expected_cost) + + row_ind, col_ind = solver(cost_matrix, maximize=maximize) + assert_array_equal(row_ind, np.sort(row_ind)) + assert_array_equal(expected_cost, + np.array(cost_matrix[row_ind, col_ind]).flatten()) + + cost_matrix = cost_matrix.T + row_ind, col_ind = solver(cost_matrix, maximize=maximize) + assert_array_equal(row_ind, np.sort(row_ind)) + assert_array_equal(np.sort(expected_cost), + np.sort(np.array( + cost_matrix[row_ind, col_ind])).flatten()) + + +linear_sum_assignment_test_cases = product( + [-1, 1], + [ + # Square + ([[400, 150, 400], + [400, 450, 600], + [300, 225, 300]], + [150, 400, 300]), + + # Rectangular variant + ([[400, 150, 400, 1], + [400, 450, 600, 2], + [300, 225, 300, 3]], + [150, 2, 300]), + + ([[10, 10, 8], + [9, 8, 1], + [9, 7, 4]], + [10, 1, 7]), + + # Square + ([[10, 10, 8, 11], + [9, 8, 1, 1], + [9, 7, 4, 10]], + [10, 1, 4]), + + # Rectangular variant + ([[10, float("inf"), float("inf")], + [float("inf"), float("inf"), 1], + [float("inf"), 7, float("inf")]], + [10, 1, 7]) + ]) + + +@pytest.mark.parametrize('sign,test_case', linear_sum_assignment_test_cases) +def test_min_weight_full_matching_small_inputs(sign, test_case): + linear_sum_assignment_assertions( + min_weight_full_bipartite_matching, csr_matrix, sign, test_case) diff --git a/tests/csgraph/test_pydata_sparse.py b/tests/csgraph/test_pydata_sparse.py new file mode 100644 index 0000000000..025aeb67c1 --- /dev/null +++ b/tests/csgraph/test_pydata_sparse.py @@ -0,0 +1,194 @@ +import pytest + +import numpy as np +import scipy.sparse as sp +import scipy.sparse.csgraph as spgraph +from scipy._lib import _pep440 + +from numpy.testing import assert_equal + +try: + import sparse +except Exception: + sparse = None + +pytestmark = pytest.mark.skipif(sparse is None, + reason="pydata/sparse not installed") + + +msg = "pydata/sparse (0.15.1) does not implement necessary operations" + + +sparse_params = (pytest.param("COO"), + pytest.param("DOK", marks=[pytest.mark.xfail(reason=msg)])) + + +def check_sparse_version(min_ver): + if sparse is None: + return pytest.mark.skip(reason="sparse is not installed") + return pytest.mark.skipif( + _pep440.parse(sparse.__version__) < _pep440.Version(min_ver), + reason=f"sparse version >= {min_ver} required" + ) + + +@pytest.fixture(params=sparse_params) +def sparse_cls(request): + return getattr(sparse, request.param) + + +@pytest.fixture +def graphs(sparse_cls): + graph = [ + [0, 1, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 0], + ] + A_dense = np.array(graph) + A_sparse = sparse_cls(A_dense) + return A_dense, A_sparse + + +@pytest.mark.parametrize( + "func", + [ + spgraph.shortest_path, + spgraph.dijkstra, + spgraph.floyd_warshall, + spgraph.bellman_ford, + spgraph.johnson, + spgraph.reverse_cuthill_mckee, + spgraph.maximum_bipartite_matching, + spgraph.structural_rank, + ] +) +def test_csgraph_equiv(func, graphs): + A_dense, A_sparse = graphs + actual = func(A_sparse) + desired = func(sp.csc_matrix(A_dense)) + assert_equal(actual, desired) + + +def test_connected_components(graphs): + A_dense, A_sparse = graphs + func = spgraph.connected_components + + actual_comp, actual_labels = func(A_sparse) + desired_comp, desired_labels, = func(sp.csc_matrix(A_dense)) + + assert actual_comp == desired_comp + assert_equal(actual_labels, desired_labels) + + +def test_laplacian(graphs): + A_dense, A_sparse = graphs + sparse_cls = type(A_sparse) + func = spgraph.laplacian + + actual = func(A_sparse) + desired = func(sp.csc_matrix(A_dense)) + + assert isinstance(actual, sparse_cls) + + assert_equal(actual.todense(), desired.todense()) + + +@pytest.mark.parametrize( + "func", [spgraph.breadth_first_order, spgraph.depth_first_order] +) +def test_order_search(graphs, func): + A_dense, A_sparse = graphs + + actual = func(A_sparse, 0) + desired = func(sp.csc_matrix(A_dense), 0) + + assert_equal(actual, desired) + + +@pytest.mark.parametrize( + "func", [spgraph.breadth_first_tree, spgraph.depth_first_tree] +) +def test_tree_search(graphs, func): + A_dense, A_sparse = graphs + sparse_cls = type(A_sparse) + + actual = func(A_sparse, 0) + desired = func(sp.csc_matrix(A_dense), 0) + + assert isinstance(actual, sparse_cls) + + assert_equal(actual.todense(), desired.todense()) + + +def test_minimum_spanning_tree(graphs): + A_dense, A_sparse = graphs + sparse_cls = type(A_sparse) + func = spgraph.minimum_spanning_tree + + actual = func(A_sparse) + desired = func(sp.csc_matrix(A_dense)) + + assert isinstance(actual, sparse_cls) + + assert_equal(actual.todense(), desired.todense()) + + +def test_maximum_flow(graphs): + A_dense, A_sparse = graphs + sparse_cls = type(A_sparse) + func = spgraph.maximum_flow + + actual = func(A_sparse, 0, 2) + desired = func(sp.csr_matrix(A_dense), 0, 2) + + assert actual.flow_value == desired.flow_value + assert isinstance(actual.flow, sparse_cls) + + assert_equal(actual.flow.todense(), desired.flow.todense()) + + +def test_min_weight_full_bipartite_matching(graphs): + A_dense, A_sparse = graphs + func = spgraph.min_weight_full_bipartite_matching + + actual = func(A_sparse[0:2, 1:3]) + desired = func(sp.csc_matrix(A_dense)[0:2, 1:3]) + + assert_equal(actual, desired) + + +@check_sparse_version("0.15.4") +@pytest.mark.parametrize( + "func", + [ + spgraph.shortest_path, + spgraph.dijkstra, + spgraph.floyd_warshall, + spgraph.bellman_ford, + spgraph.johnson, + spgraph.minimum_spanning_tree, + ] +) +@pytest.mark.parametrize( + "fill_value, comp_func", + [(np.inf, np.isposinf), (np.nan, np.isnan)], +) +def test_nonzero_fill_value(graphs, func, fill_value, comp_func): + A_dense, A_sparse = graphs + A_sparse = A_sparse.astype(float) + A_sparse.fill_value = fill_value + sparse_cls = type(A_sparse) + + actual = func(A_sparse) + desired = func(sp.csc_matrix(A_dense)) + + if func == spgraph.minimum_spanning_tree: + assert isinstance(actual, sparse_cls) + assert comp_func(actual.fill_value) + actual = actual.todense() + actual[comp_func(actual)] = 0.0 + assert_equal(actual, desired.todense()) + else: + assert_equal(actual, desired) diff --git a/tests/csgraph/test_reordering.py b/tests/csgraph/test_reordering.py new file mode 100644 index 0000000000..cb4c002fa3 --- /dev/null +++ b/tests/csgraph/test_reordering.py @@ -0,0 +1,70 @@ +import numpy as np +from numpy.testing import assert_equal +from scipy.sparse.csgraph import reverse_cuthill_mckee, structural_rank +from scipy.sparse import csc_matrix, csr_matrix, coo_matrix + + +def test_graph_reverse_cuthill_mckee(): + A = np.array([[1, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 0, 0, 1, 0, 1], + [0, 1, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 1, 0], + [1, 0, 1, 0, 1, 0, 0, 0], + [0, 1, 0, 0, 0, 1, 0, 1], + [0, 0, 0, 1, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0, 1]], dtype=int) + + graph = csr_matrix(A) + perm = reverse_cuthill_mckee(graph) + correct_perm = np.array([6, 3, 7, 5, 1, 2, 4, 0]) + assert_equal(perm, correct_perm) + + # Test int64 indices input + graph.indices = graph.indices.astype('int64') + graph.indptr = graph.indptr.astype('int64') + perm = reverse_cuthill_mckee(graph, True) + assert_equal(perm, correct_perm) + + +def test_graph_reverse_cuthill_mckee_ordering(): + data = np.ones(63,dtype=int) + rows = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, + 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, + 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, + 9, 10, 10, 10, 10, 10, 11, 11, 11, 11, + 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, + 14, 15, 15, 15, 15, 15]) + cols = np.array([0, 2, 5, 8, 10, 1, 3, 9, 11, 0, 2, + 7, 10, 1, 3, 11, 4, 6, 12, 14, 0, 7, 13, + 15, 4, 6, 14, 2, 5, 7, 15, 0, 8, 10, 13, + 1, 9, 11, 0, 2, 8, 10, 15, 1, 3, 9, 11, + 4, 12, 14, 5, 8, 13, 15, 4, 6, 12, 14, + 5, 7, 10, 13, 15]) + graph = coo_matrix((data, (rows,cols))).tocsr() + perm = reverse_cuthill_mckee(graph) + correct_perm = np.array([12, 14, 4, 6, 10, 8, 2, 15, + 0, 13, 7, 5, 9, 11, 1, 3]) + assert_equal(perm, correct_perm) + + +def test_graph_structural_rank(): + # Test square matrix #1 + A = csc_matrix([[1, 1, 0], + [1, 0, 1], + [0, 1, 0]]) + assert_equal(structural_rank(A), 3) + + # Test square matrix #2 + rows = np.array([0,0,0,0,0,1,1,2,2,3,3,3,3,3,3,4,4,5,5,6,6,7,7]) + cols = np.array([0,1,2,3,4,2,5,2,6,0,1,3,5,6,7,4,5,5,6,2,6,2,4]) + data = np.ones_like(rows) + B = coo_matrix((data,(rows,cols)), shape=(8,8)) + assert_equal(structural_rank(B), 6) + + #Test non-square matrix + C = csc_matrix([[1, 0, 2, 0], + [2, 0, 4, 0]]) + assert_equal(structural_rank(C), 2) + + #Test tall matrix + assert_equal(structural_rank(C.T), 2) diff --git a/tests/csgraph/test_shortest_path.py b/tests/csgraph/test_shortest_path.py new file mode 100644 index 0000000000..45600352e8 --- /dev/null +++ b/tests/csgraph/test_shortest_path.py @@ -0,0 +1,484 @@ +from io import StringIO +import warnings +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_allclose +from pytest import raises as assert_raises +from scipy.sparse.csgraph import (shortest_path, dijkstra, johnson, + bellman_ford, construct_dist_matrix, yen, + NegativeCycleError) +import scipy.sparse +from scipy.io import mmread +import pytest + +directed_G = np.array([[0, 3, 3, 0, 0], + [0, 0, 0, 2, 4], + [0, 0, 0, 0, 0], + [1, 0, 0, 0, 0], + [2, 0, 0, 2, 0]], dtype=float) + +undirected_G = np.array([[0, 3, 3, 1, 2], + [3, 0, 0, 2, 4], + [3, 0, 0, 0, 0], + [1, 2, 0, 0, 2], + [2, 4, 0, 2, 0]], dtype=float) + +unweighted_G = (directed_G > 0).astype(float) + +directed_SP = [[0, 3, 3, 5, 7], + [3, 0, 6, 2, 4], + [np.inf, np.inf, 0, np.inf, np.inf], + [1, 4, 4, 0, 8], + [2, 5, 5, 2, 0]] + +directed_2SP_0_to_3 = [[-9999, 0, -9999, 1, -9999], + [-9999, 0, -9999, 4, 1]] + +directed_sparse_zero_G = scipy.sparse.csr_matrix( + ( + [0, 1, 2, 3, 1], + ([0, 1, 2, 3, 4], [1, 2, 0, 4, 3]), + ), + shape=(5, 5), +) + +directed_sparse_zero_SP = [[0, 0, 1, np.inf, np.inf], + [3, 0, 1, np.inf, np.inf], + [2, 2, 0, np.inf, np.inf], + [np.inf, np.inf, np.inf, 0, 3], + [np.inf, np.inf, np.inf, 1, 0]] + +undirected_sparse_zero_G = scipy.sparse.csr_matrix( + ( + [0, 0, 1, 1, 2, 2, 1, 1], + ([0, 1, 1, 2, 2, 0, 3, 4], [1, 0, 2, 1, 0, 2, 4, 3]) + ), + shape=(5, 5), +) + +undirected_sparse_zero_SP = [[0, 0, 1, np.inf, np.inf], + [0, 0, 1, np.inf, np.inf], + [1, 1, 0, np.inf, np.inf], + [np.inf, np.inf, np.inf, 0, 1], + [np.inf, np.inf, np.inf, 1, 0]] + +directed_pred = np.array([[-9999, 0, 0, 1, 1], + [3, -9999, 0, 1, 1], + [-9999, -9999, -9999, -9999, -9999], + [3, 0, 0, -9999, 1], + [4, 0, 0, 4, -9999]], dtype=float) + +undirected_SP = np.array([[0, 3, 3, 1, 2], + [3, 0, 6, 2, 4], + [3, 6, 0, 4, 5], + [1, 2, 4, 0, 2], + [2, 4, 5, 2, 0]], dtype=float) + +undirected_SP_limit_2 = np.array([[0, np.inf, np.inf, 1, 2], + [np.inf, 0, np.inf, 2, np.inf], + [np.inf, np.inf, 0, np.inf, np.inf], + [1, 2, np.inf, 0, 2], + [2, np.inf, np.inf, 2, 0]], dtype=float) + +undirected_SP_limit_0 = np.ones((5, 5), dtype=float) - np.eye(5) +undirected_SP_limit_0[undirected_SP_limit_0 > 0] = np.inf + +undirected_pred = np.array([[-9999, 0, 0, 0, 0], + [1, -9999, 0, 1, 1], + [2, 0, -9999, 0, 0], + [3, 3, 0, -9999, 3], + [4, 4, 0, 4, -9999]], dtype=float) + +directed_negative_weighted_G = np.array([[0, 0, 0], + [-1, 0, 0], + [0, -1, 0]], dtype=float) + +directed_negative_weighted_SP = np.array([[0, np.inf, np.inf], + [-1, 0, np.inf], + [-2, -1, 0]], dtype=float) + +methods = ['auto', 'FW', 'D', 'BF', 'J'] + + +def test_dijkstra_limit(): + limits = [0, 2, np.inf] + results = [undirected_SP_limit_0, + undirected_SP_limit_2, + undirected_SP] + + def check(limit, result): + SP = dijkstra(undirected_G, directed=False, limit=limit) + assert_array_almost_equal(SP, result) + + for limit, result in zip(limits, results): + check(limit, result) + + +def test_directed(): + def check(method): + SP = shortest_path(directed_G, method=method, directed=True, + overwrite=False) + assert_array_almost_equal(SP, directed_SP) + + for method in methods: + check(method) + + +def test_undirected(): + def check(method, directed_in): + if directed_in: + SP1 = shortest_path(directed_G, method=method, directed=False, + overwrite=False) + assert_array_almost_equal(SP1, undirected_SP) + else: + SP2 = shortest_path(undirected_G, method=method, directed=True, + overwrite=False) + assert_array_almost_equal(SP2, undirected_SP) + + for method in methods: + for directed_in in (True, False): + check(method, directed_in) + + +def test_directed_sparse_zero(): + # test directed sparse graph with zero-weight edge and two connected components + def check(method): + SP = shortest_path(directed_sparse_zero_G, method=method, directed=True, + overwrite=False) + assert_array_almost_equal(SP, directed_sparse_zero_SP) + + for method in methods: + check(method) + + +def test_undirected_sparse_zero(): + def check(method, directed_in): + if directed_in: + SP1 = shortest_path(directed_sparse_zero_G, method=method, directed=False, + overwrite=False) + assert_array_almost_equal(SP1, undirected_sparse_zero_SP) + else: + SP2 = shortest_path(undirected_sparse_zero_G, method=method, directed=True, + overwrite=False) + assert_array_almost_equal(SP2, undirected_sparse_zero_SP) + + for method in methods: + for directed_in in (True, False): + check(method, directed_in) + + +@pytest.mark.parametrize('directed, SP_ans', + ((True, directed_SP), + (False, undirected_SP))) +@pytest.mark.parametrize('indices', ([0, 2, 4], [0, 4], [3, 4], [0, 0])) +def test_dijkstra_indices_min_only(directed, SP_ans, indices): + SP_ans = np.array(SP_ans) + indices = np.array(indices, dtype=np.int64) + min_ind_ans = indices[np.argmin(SP_ans[indices, :], axis=0)] + min_d_ans = np.zeros(SP_ans.shape[0], SP_ans.dtype) + for k in range(SP_ans.shape[0]): + min_d_ans[k] = SP_ans[min_ind_ans[k], k] + min_ind_ans[np.isinf(min_d_ans)] = -9999 + + SP, pred, sources = dijkstra(directed_G, + directed=directed, + indices=indices, + min_only=True, + return_predecessors=True) + assert_array_almost_equal(SP, min_d_ans) + assert_array_equal(min_ind_ans, sources) + SP = dijkstra(directed_G, + directed=directed, + indices=indices, + min_only=True, + return_predecessors=False) + assert_array_almost_equal(SP, min_d_ans) + + +@pytest.mark.parametrize('n', (10, 100, 1000)) +def test_dijkstra_min_only_random(n): + np.random.seed(1234) + data = scipy.sparse.rand(n, n, density=0.5, format='lil', + random_state=42, dtype=np.float64) + data.setdiag(np.zeros(n, dtype=np.bool_)) + # choose some random vertices + v = np.arange(n) + np.random.shuffle(v) + indices = v[:int(n*.1)] + ds, pred, sources = dijkstra(data, + directed=True, + indices=indices, + min_only=True, + return_predecessors=True) + for k in range(n): + p = pred[k] + s = sources[k] + while p != -9999: + assert sources[p] == s + p = pred[p] + + +def test_dijkstra_random(): + # reproduces the hang observed in gh-17782 + n = 10 + indices = [0, 4, 4, 5, 7, 9, 0, 6, 2, 3, 7, 9, 1, 2, 9, 2, 5, 6] + indptr = [0, 0, 2, 5, 6, 7, 8, 12, 15, 18, 18] + data = [0.33629, 0.40458, 0.47493, 0.42757, 0.11497, 0.91653, 0.69084, + 0.64979, 0.62555, 0.743, 0.01724, 0.99945, 0.31095, 0.15557, + 0.02439, 0.65814, 0.23478, 0.24072] + graph = scipy.sparse.csr_matrix((data, indices, indptr), shape=(n, n)) + dijkstra(graph, directed=True, return_predecessors=True) + + +def test_gh_17782_segfault(): + text = """%%MatrixMarket matrix coordinate real general + 84 84 22 + 2 1 4.699999809265137e+00 + 6 14 1.199999973177910e-01 + 9 6 1.199999973177910e-01 + 10 16 2.012000083923340e+01 + 11 10 1.422000026702881e+01 + 12 1 9.645999908447266e+01 + 13 18 2.012000083923340e+01 + 14 13 4.679999828338623e+00 + 15 11 1.199999973177910e-01 + 16 12 1.199999973177910e-01 + 18 15 1.199999973177910e-01 + 32 2 2.299999952316284e+00 + 33 20 6.000000000000000e+00 + 33 32 5.000000000000000e+00 + 36 9 3.720000028610229e+00 + 36 37 3.720000028610229e+00 + 36 38 3.720000028610229e+00 + 37 44 8.159999847412109e+00 + 38 32 7.903999328613281e+01 + 43 20 2.400000000000000e+01 + 43 33 4.000000000000000e+00 + 44 43 6.028000259399414e+01 + """ + data = mmread(StringIO(text)) + dijkstra(data, directed=True, return_predecessors=True) + + +def test_shortest_path_indices(): + indices = np.arange(4) + + def check(func, indshape): + outshape = indshape + (5,) + SP = func(directed_G, directed=False, + indices=indices.reshape(indshape)) + assert_array_almost_equal(SP, undirected_SP[indices].reshape(outshape)) + + for indshape in [(4,), (4, 1), (2, 2)]: + for func in (dijkstra, bellman_ford, johnson, shortest_path): + check(func, indshape) + + assert_raises(ValueError, shortest_path, directed_G, method='FW', + indices=indices) + + +def test_predecessors(): + SP_res = {True: directed_SP, + False: undirected_SP} + pred_res = {True: directed_pred, + False: undirected_pred} + + def check(method, directed): + SP, pred = shortest_path(directed_G, method, directed=directed, + overwrite=False, + return_predecessors=True) + assert_array_almost_equal(SP, SP_res[directed]) + assert_array_almost_equal(pred, pred_res[directed]) + + for method in methods: + for directed in (True, False): + check(method, directed) + + +def test_construct_shortest_path(): + def check(method, directed): + SP1, pred = shortest_path(directed_G, + directed=directed, + overwrite=False, + return_predecessors=True) + SP2 = construct_dist_matrix(directed_G, pred, directed=directed) + assert_array_almost_equal(SP1, SP2) + + for method in methods: + for directed in (True, False): + check(method, directed) + + +def test_unweighted_path(): + def check(method, directed): + SP1 = shortest_path(directed_G, + directed=directed, + overwrite=False, + unweighted=True) + SP2 = shortest_path(unweighted_G, + directed=directed, + overwrite=False, + unweighted=False) + assert_array_almost_equal(SP1, SP2) + + for method in methods: + for directed in (True, False): + check(method, directed) + + +def test_negative_cycles(): + # create a small graph with a negative cycle + graph = np.ones([5, 5]) + graph.flat[::6] = 0 + graph[1, 2] = -2 + + def check(method, directed): + assert_raises(NegativeCycleError, shortest_path, graph, method, + directed) + + for directed in (True, False): + for method in ['FW', 'J', 'BF']: + check(method, directed) + + assert_raises(NegativeCycleError, yen, graph, 0, 1, 1, + directed=directed) + + +@pytest.mark.parametrize("method", ['FW', 'J', 'BF']) +def test_negative_weights(method): + SP = shortest_path(directed_negative_weighted_G, method, directed=True) + assert_allclose(SP, directed_negative_weighted_SP, atol=1e-10) + + +def test_masked_input(): + np.ma.masked_equal(directed_G, 0) + + def check(method): + SP = shortest_path(directed_G, method=method, directed=True, + overwrite=False) + assert_array_almost_equal(SP, directed_SP) + + for method in methods: + check(method) + + +def test_overwrite(): + G = np.array([[0, 3, 3, 1, 2], + [3, 0, 0, 2, 4], + [3, 0, 0, 0, 0], + [1, 2, 0, 0, 2], + [2, 4, 0, 2, 0]], dtype=float) + foo = G.copy() + shortest_path(foo, overwrite=False) + assert_array_equal(foo, G) + + +@pytest.mark.parametrize('method', methods) +def test_buffer(method): + # Smoke test that sparse matrices with read-only buffers (e.g., those from + # joblib workers) do not cause:: + # + # ValueError: buffer source array is read-only + # + G = scipy.sparse.csr_matrix([[1.]]) + G.data.flags['WRITEABLE'] = False + shortest_path(G, method=method) + + +def test_NaN_warnings(): + with warnings.catch_warnings(record=True) as record: + shortest_path(np.array([[0, 1], [np.nan, 0]])) + for r in record: + assert r.category is not RuntimeWarning + + +def test_sparse_matrices(): + # Test that using lil,csr and csc sparse matrix do not cause error + G_dense = np.array([[0, 3, 0, 0, 0], + [0, 0, -1, 0, 0], + [0, 0, 0, 2, 0], + [0, 0, 0, 0, 4], + [0, 0, 0, 0, 0]], dtype=float) + SP = shortest_path(G_dense) + G_csr = scipy.sparse.csr_matrix(G_dense) + G_csc = scipy.sparse.csc_matrix(G_dense) + G_lil = scipy.sparse.lil_matrix(G_dense) + assert_array_almost_equal(SP, shortest_path(G_csr)) + assert_array_almost_equal(SP, shortest_path(G_csc)) + assert_array_almost_equal(SP, shortest_path(G_lil)) + + +def test_yen_directed(): + distances, predecessors = yen( + directed_G, + source=0, + sink=3, + K=2, + return_predecessors=True + ) + assert_allclose(distances, [5., 9.]) + assert_allclose(predecessors, directed_2SP_0_to_3) + + +def test_yen_undirected(): + distances = yen( + undirected_G, + source=0, + sink=3, + K=4, + ) + assert_allclose(distances, [1., 4., 5., 8.]) + +def test_yen_unweighted(): + # Ask for more paths than there are, verify only the available paths are returned + distances, predecessors = yen( + directed_G, + source=0, + sink=3, + K=4, + unweighted=True, + return_predecessors=True, + ) + assert_allclose(distances, [2., 3.]) + assert_allclose(predecessors, directed_2SP_0_to_3) + +def test_yen_no_paths(): + distances = yen( + directed_G, + source=2, + sink=3, + K=1, + ) + assert distances.size == 0 + +def test_yen_negative_weights(): + distances = yen( + directed_negative_weighted_G, + source=2, + sink=0, + K=1, + ) + assert_allclose(distances, [-2.]) + + +@pytest.mark.parametrize("min_only", (True, False)) +@pytest.mark.parametrize("directed", (True, False)) +@pytest.mark.parametrize("return_predecessors", (True, False)) +@pytest.mark.parametrize("index_dtype", (np.int32, np.int64)) +@pytest.mark.parametrize("indices", (None, [1])) +def test_20904(min_only, directed, return_predecessors, index_dtype, indices): + """Test two failures from gh-20904: int32 and indices-as-None.""" + adj_mat = scipy.sparse.eye(4, format="csr") + adj_mat = scipy.sparse.csr_array( + ( + adj_mat.data, + adj_mat.indices.astype(index_dtype), + adj_mat.indptr.astype(index_dtype), + ), + ) + dijkstra( + adj_mat, + directed, + indices=indices, + min_only=min_only, + return_predecessors=return_predecessors, + ) diff --git a/tests/csgraph/test_spanning_tree.py b/tests/csgraph/test_spanning_tree.py new file mode 100644 index 0000000000..90ef6d1b1b --- /dev/null +++ b/tests/csgraph/test_spanning_tree.py @@ -0,0 +1,66 @@ +"""Test the minimum spanning tree function""" +import numpy as np +from numpy.testing import assert_ +import numpy.testing as npt +from scipy.sparse import csr_matrix +from scipy.sparse.csgraph import minimum_spanning_tree + + +def test_minimum_spanning_tree(): + + # Create a graph with two connected components. + graph = [[0,1,0,0,0], + [1,0,0,0,0], + [0,0,0,8,5], + [0,0,8,0,1], + [0,0,5,1,0]] + graph = np.asarray(graph) + + # Create the expected spanning tree. + expected = [[0,1,0,0,0], + [0,0,0,0,0], + [0,0,0,0,5], + [0,0,0,0,1], + [0,0,0,0,0]] + expected = np.asarray(expected) + + # Ensure minimum spanning tree code gives this expected output. + csgraph = csr_matrix(graph) + mintree = minimum_spanning_tree(csgraph) + mintree_array = mintree.toarray() + npt.assert_array_equal(mintree_array, expected, + 'Incorrect spanning tree found.') + + # Ensure that the original graph was not modified. + npt.assert_array_equal(csgraph.toarray(), graph, + 'Original graph was modified.') + + # Now let the algorithm modify the csgraph in place. + mintree = minimum_spanning_tree(csgraph, overwrite=True) + npt.assert_array_equal(mintree.toarray(), expected, + 'Graph was not properly modified to contain MST.') + + np.random.seed(1234) + for N in (5, 10, 15, 20): + + # Create a random graph. + graph = 3 + np.random.random((N, N)) + csgraph = csr_matrix(graph) + + # The spanning tree has at most N - 1 edges. + mintree = minimum_spanning_tree(csgraph) + assert_(mintree.nnz < N) + + # Set the sub diagonal to 1 to create a known spanning tree. + idx = np.arange(N-1) + graph[idx,idx+1] = 1 + csgraph = csr_matrix(graph) + mintree = minimum_spanning_tree(csgraph) + + # We expect to see this pattern in the spanning tree and otherwise + # have this zero. + expected = np.zeros((N, N)) + expected[idx, idx+1] = 1 + + npt.assert_array_equal(mintree.toarray(), expected, + 'Incorrect spanning tree found.') diff --git a/tests/csgraph/test_traversal.py b/tests/csgraph/test_traversal.py new file mode 100644 index 0000000000..414e2d1486 --- /dev/null +++ b/tests/csgraph/test_traversal.py @@ -0,0 +1,81 @@ +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal +from scipy.sparse import csr_array +from scipy.sparse.csgraph import (breadth_first_tree, depth_first_tree, + csgraph_to_dense, csgraph_from_dense) + + +def test_graph_breadth_first(): + csgraph = np.array([[0, 1, 2, 0, 0], + [1, 0, 0, 0, 3], + [2, 0, 0, 7, 0], + [0, 0, 7, 0, 1], + [0, 3, 0, 1, 0]]) + csgraph = csgraph_from_dense(csgraph, null_value=0) + + bfirst = np.array([[0, 1, 2, 0, 0], + [0, 0, 0, 0, 3], + [0, 0, 0, 7, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]) + + for directed in [True, False]: + bfirst_test = breadth_first_tree(csgraph, 0, directed) + assert_array_almost_equal(csgraph_to_dense(bfirst_test), + bfirst) + + +def test_graph_depth_first(): + csgraph = np.array([[0, 1, 2, 0, 0], + [1, 0, 0, 0, 3], + [2, 0, 0, 7, 0], + [0, 0, 7, 0, 1], + [0, 3, 0, 1, 0]]) + csgraph = csgraph_from_dense(csgraph, null_value=0) + + dfirst = np.array([[0, 1, 0, 0, 0], + [0, 0, 0, 0, 3], + [0, 0, 0, 0, 0], + [0, 0, 7, 0, 0], + [0, 0, 0, 1, 0]]) + + for directed in [True, False]: + dfirst_test = depth_first_tree(csgraph, 0, directed) + assert_array_almost_equal(csgraph_to_dense(dfirst_test), + dfirst) + + +def test_graph_breadth_first_trivial_graph(): + csgraph = np.array([[0]]) + csgraph = csgraph_from_dense(csgraph, null_value=0) + + bfirst = np.array([[0]]) + + for directed in [True, False]: + bfirst_test = breadth_first_tree(csgraph, 0, directed) + assert_array_almost_equal(csgraph_to_dense(bfirst_test), + bfirst) + + +def test_graph_depth_first_trivial_graph(): + csgraph = np.array([[0]]) + csgraph = csgraph_from_dense(csgraph, null_value=0) + + bfirst = np.array([[0]]) + + for directed in [True, False]: + bfirst_test = depth_first_tree(csgraph, 0, directed) + assert_array_almost_equal(csgraph_to_dense(bfirst_test), + bfirst) + + +@pytest.mark.parametrize('directed', [True, False]) +@pytest.mark.parametrize('tree_func', [breadth_first_tree, depth_first_tree]) +def test_int64_indices(tree_func, directed): + # See https://github.com/scipy/scipy/issues/18716 + g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2)) + assert g.indices.dtype == np.int64 + tree = tree_func(g, 0, directed=directed) + assert_array_almost_equal(csgraph_to_dense(tree), [[0, 1], [0, 0]]) + From 4ae17dc5f07a6a51dac158864fcb18b70e082509 Mon Sep 17 00:00:00 2001 From: Allen Goodman Date: Fri, 4 Oct 2024 09:58:54 -0400 Subject: [PATCH 2/8] graph operators --- src/beignet/_validate_graph_matrix.py | 22 ++++++++++++++----- .../test__graph_matrix_to_masked_tensor.py | 2 ++ tests/beignet/test__graph_matrix_to_tensor.py | 2 ++ .../test__masked_tensor_to_graph_matrix.py | 2 ++ ...__predecessor_matrix_to_distance_matrix.py | 2 ++ tests/beignet/test__reconstruct_path.py | 2 ++ tests/beignet/test__tensor_to_graph_matrix.py | 2 ++ .../test__tensor_to_masked_graph_matrix.py | 2 ++ tests/beignet/test__validate_graph_matrix.py | 2 ++ 9 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/beignet/_validate_graph_matrix.py b/src/beignet/_validate_graph_matrix.py index e6ceba3cff..89a2c86679 100644 --- a/src/beignet/_validate_graph_matrix.py +++ b/src/beignet/_validate_graph_matrix.py @@ -21,9 +21,8 @@ def validate_graph_matrix( nan_null=True, dtype=torch.float64, ): - """Routine for validation and conversion of csgraph inputs""" if not (csr_output or dense_output): - raise ValueError("Internal: dense or csr output must be true") + raise ValueError accept_fv = [null_value_in] @@ -54,7 +53,13 @@ def validate_graph_matrix( graph = masked_tensor_to_graph_matrix(graph) else: if dense_output: - graph = tensor_to_masked_graph_matrix(graph, copy=copy_if_dense, null_value=null_value_in, nan_null=nan_null, infinity_null=infinity_null) + graph = tensor_to_masked_graph_matrix( + graph, + copy=copy_if_dense, + null_value=null_value_in, + nan_null=nan_null, + infinity_null=infinity_null, + ) mask = graph.mask @@ -62,12 +67,17 @@ def validate_graph_matrix( graph[mask] = null_value_out else: - graph = tensor_to_graph_matrix(graph, null_value=null_value_in, infinity_is_null_edge=infinity_null, nan_is_null_edge=nan_null) + graph = tensor_to_graph_matrix( + graph, + null_value=null_value_in, + infinity_is_null_edge=infinity_null, + nan_is_null_edge=nan_null, + ) if graph.ndim != 2: - raise ValueError("compressed-sparse graph must be 2-D") + raise ValueError if graph.shape[0] != graph.shape[1]: - raise ValueError("compressed-sparse graph must be shape (N, N)") + raise ValueError return graph diff --git a/tests/beignet/test__graph_matrix_to_masked_tensor.py b/tests/beignet/test__graph_matrix_to_masked_tensor.py index e69de29bb2..c99149ac4b 100644 --- a/tests/beignet/test__graph_matrix_to_masked_tensor.py +++ b/tests/beignet/test__graph_matrix_to_masked_tensor.py @@ -0,0 +1,2 @@ +def test_graph_matrix_to_masked_tensor(): + assert False \ No newline at end of file diff --git a/tests/beignet/test__graph_matrix_to_tensor.py b/tests/beignet/test__graph_matrix_to_tensor.py index e69de29bb2..0db4c6e639 100644 --- a/tests/beignet/test__graph_matrix_to_tensor.py +++ b/tests/beignet/test__graph_matrix_to_tensor.py @@ -0,0 +1,2 @@ +def test_graph_matrix_to_tensor(): + assert False diff --git a/tests/beignet/test__masked_tensor_to_graph_matrix.py b/tests/beignet/test__masked_tensor_to_graph_matrix.py index e69de29bb2..7450b01b7a 100644 --- a/tests/beignet/test__masked_tensor_to_graph_matrix.py +++ b/tests/beignet/test__masked_tensor_to_graph_matrix.py @@ -0,0 +1,2 @@ +def test_masked_tensor_to_graph_matrix(): + assert False diff --git a/tests/beignet/test__predecessor_matrix_to_distance_matrix.py b/tests/beignet/test__predecessor_matrix_to_distance_matrix.py index e69de29bb2..4a558ddc51 100644 --- a/tests/beignet/test__predecessor_matrix_to_distance_matrix.py +++ b/tests/beignet/test__predecessor_matrix_to_distance_matrix.py @@ -0,0 +1,2 @@ +def test_predecessor_matrix_to_distance_matrix(): + assert False \ No newline at end of file diff --git a/tests/beignet/test__reconstruct_path.py b/tests/beignet/test__reconstruct_path.py index e69de29bb2..798c8499df 100644 --- a/tests/beignet/test__reconstruct_path.py +++ b/tests/beignet/test__reconstruct_path.py @@ -0,0 +1,2 @@ +def test_reconstruct_path(): + assert False diff --git a/tests/beignet/test__tensor_to_graph_matrix.py b/tests/beignet/test__tensor_to_graph_matrix.py index e69de29bb2..8d03fff881 100644 --- a/tests/beignet/test__tensor_to_graph_matrix.py +++ b/tests/beignet/test__tensor_to_graph_matrix.py @@ -0,0 +1,2 @@ +def test_tensor_to_graph_matrix(): + assert False \ No newline at end of file diff --git a/tests/beignet/test__tensor_to_masked_graph_matrix.py b/tests/beignet/test__tensor_to_masked_graph_matrix.py index e69de29bb2..2fadb3b68f 100644 --- a/tests/beignet/test__tensor_to_masked_graph_matrix.py +++ b/tests/beignet/test__tensor_to_masked_graph_matrix.py @@ -0,0 +1,2 @@ +def test_tensor_to_masked_graph_matrix(): + assert False \ No newline at end of file diff --git a/tests/beignet/test__validate_graph_matrix.py b/tests/beignet/test__validate_graph_matrix.py index e69de29bb2..ab8c139f2c 100644 --- a/tests/beignet/test__validate_graph_matrix.py +++ b/tests/beignet/test__validate_graph_matrix.py @@ -0,0 +1,2 @@ +def test_validate_graph_matrix(): + assert False From b9136a3a54e2a3b241ef4fce648329a119590e3e Mon Sep 17 00:00:00 2001 From: Allen Goodman Date: Fri, 4 Oct 2024 09:59:38 -0400 Subject: [PATCH 3/8] graph operators --- tests/csgraph/__init__.py | 0 tests/csgraph/meson.build | 20 - tests/csgraph/test_connected_components.py | 119 ----- tests/csgraph/test_conversions.py | 61 --- tests/csgraph/test_flow.py | 201 --------- tests/csgraph/test_graph_laplacian.py | 368 ---------------- tests/csgraph/test_matching.py | 294 ------------- tests/csgraph/test_pydata_sparse.py | 194 --------- tests/csgraph/test_reordering.py | 70 --- tests/csgraph/test_shortest_path.py | 484 --------------------- tests/csgraph/test_spanning_tree.py | 66 --- tests/csgraph/test_traversal.py | 81 ---- 12 files changed, 1958 deletions(-) delete mode 100644 tests/csgraph/__init__.py delete mode 100644 tests/csgraph/meson.build delete mode 100644 tests/csgraph/test_connected_components.py delete mode 100644 tests/csgraph/test_conversions.py delete mode 100644 tests/csgraph/test_flow.py delete mode 100644 tests/csgraph/test_graph_laplacian.py delete mode 100644 tests/csgraph/test_matching.py delete mode 100644 tests/csgraph/test_pydata_sparse.py delete mode 100644 tests/csgraph/test_reordering.py delete mode 100644 tests/csgraph/test_shortest_path.py delete mode 100644 tests/csgraph/test_spanning_tree.py delete mode 100644 tests/csgraph/test_traversal.py diff --git a/tests/csgraph/__init__.py b/tests/csgraph/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/csgraph/meson.build b/tests/csgraph/meson.build deleted file mode 100644 index 7ad02ba3b2..0000000000 --- a/tests/csgraph/meson.build +++ /dev/null @@ -1,20 +0,0 @@ -python_sources = [ - '__init__.py', - 'test_connected_components.py', - 'test_conversions.py', - 'test_flow.py', - 'test_graph_laplacian.py', - 'test_matching.py', - 'test_pydata_sparse.py', - 'test_reordering.py', - 'test_shortest_path.py', - 'test_spanning_tree.py', - 'test_traversal.py' -] - - -py3.install_sources( - python_sources, - subdir: 'scipy/sparse/csgraph/tests', - install_tag: 'tests' -) diff --git a/tests/csgraph/test_connected_components.py b/tests/csgraph/test_connected_components.py deleted file mode 100644 index 0b190a24de..0000000000 --- a/tests/csgraph/test_connected_components.py +++ /dev/null @@ -1,119 +0,0 @@ -import numpy as np -from numpy.testing import assert_equal, assert_array_almost_equal -from scipy.sparse import csgraph, csr_array - - -def test_weak_connections(): - Xde = np.array([[0, 1, 0], - [0, 0, 0], - [0, 0, 0]]) - - Xsp = csgraph.csgraph_from_dense(Xde, null_value=0) - - for X in Xsp, Xde: - n_components, labels =\ - csgraph.connected_components(X, directed=True, - connection='weak') - - assert_equal(n_components, 2) - assert_array_almost_equal(labels, [0, 0, 1]) - - -def test_strong_connections(): - X1de = np.array([[0, 1, 0], - [0, 0, 0], - [0, 0, 0]]) - X2de = X1de + X1de.T - - X1sp = csgraph.csgraph_from_dense(X1de, null_value=0) - X2sp = csgraph.csgraph_from_dense(X2de, null_value=0) - - for X in X1sp, X1de: - n_components, labels =\ - csgraph.connected_components(X, directed=True, - connection='strong') - - assert_equal(n_components, 3) - labels.sort() - assert_array_almost_equal(labels, [0, 1, 2]) - - for X in X2sp, X2de: - n_components, labels =\ - csgraph.connected_components(X, directed=True, - connection='strong') - - assert_equal(n_components, 2) - labels.sort() - assert_array_almost_equal(labels, [0, 0, 1]) - - -def test_strong_connections2(): - X = np.array([[0, 0, 0, 0, 0, 0], - [1, 0, 1, 0, 0, 0], - [0, 0, 0, 1, 0, 0], - [0, 0, 1, 0, 1, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0]]) - n_components, labels =\ - csgraph.connected_components(X, directed=True, - connection='strong') - assert_equal(n_components, 5) - labels.sort() - assert_array_almost_equal(labels, [0, 1, 2, 2, 3, 4]) - - -def test_weak_connections2(): - X = np.array([[0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0], - [0, 0, 1, 0, 1, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0]]) - n_components, labels =\ - csgraph.connected_components(X, directed=True, - connection='weak') - assert_equal(n_components, 2) - labels.sort() - assert_array_almost_equal(labels, [0, 0, 1, 1, 1, 1]) - - -def test_ticket1876(): - # Regression test: this failed in the original implementation - # There should be two strongly-connected components; previously gave one - g = np.array([[0, 1, 1, 0], - [1, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 1, 0]]) - n_components, labels = csgraph.connected_components(g, connection='strong') - - assert_equal(n_components, 2) - assert_equal(labels[0], labels[1]) - assert_equal(labels[2], labels[3]) - - -def test_fully_connected_graph(): - # Fully connected dense matrices raised an exception. - # https://github.com/scipy/scipy/issues/3818 - g = np.ones((4, 4)) - n_components, labels = csgraph.connected_components(g) - assert_equal(n_components, 1) - - -def test_int64_indices_undirected(): - # See https://github.com/scipy/scipy/issues/18716 - g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2)) - assert g.indices.dtype == np.int64 - n, labels = csgraph.connected_components(g, directed=False) - assert n == 1 - assert_array_almost_equal(labels, [0, 0]) - - -def test_int64_indices_directed(): - # See https://github.com/scipy/scipy/issues/18716 - g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2)) - assert g.indices.dtype == np.int64 - n, labels = csgraph.connected_components(g, directed=True, - connection='strong') - assert n == 2 - assert_array_almost_equal(labels, [1, 0]) - diff --git a/tests/csgraph/test_conversions.py b/tests/csgraph/test_conversions.py deleted file mode 100644 index e7900d67b5..0000000000 --- a/tests/csgraph/test_conversions.py +++ /dev/null @@ -1,61 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_almost_equal -from scipy.sparse import csr_matrix -from scipy.sparse.csgraph import csgraph_from_dense, csgraph_to_dense - - -def test_csgraph_from_dense(): - np.random.seed(1234) - G = np.random.random((10, 10)) - some_nulls = (G < 0.4) - all_nulls = (G < 0.8) - - for null_value in [0, np.nan, np.inf]: - G[all_nulls] = null_value - with np.errstate(invalid="ignore"): - G_csr = csgraph_from_dense(G, null_value=0) - - G[all_nulls] = 0 - assert_array_almost_equal(G, G_csr.toarray()) - - for null_value in [np.nan, np.inf]: - G[all_nulls] = 0 - G[some_nulls] = null_value - with np.errstate(invalid="ignore"): - G_csr = csgraph_from_dense(G, null_value=0) - - G[all_nulls] = 0 - assert_array_almost_equal(G, G_csr.toarray()) - - -def test_csgraph_to_dense(): - np.random.seed(1234) - G = np.random.random((10, 10)) - nulls = (G < 0.8) - G[nulls] = np.inf - - G_csr = csgraph_from_dense(G) - - for null_value in [0, 10, -np.inf, np.inf]: - G[nulls] = null_value - assert_array_almost_equal(G, csgraph_to_dense(G_csr, null_value)) - - -def test_multiple_edges(): - # create a random square matrix with an even number of elements - np.random.seed(1234) - X = np.random.random((10, 10)) - Xcsr = csr_matrix(X) - - # now double-up every other column - Xcsr.indices[::2] = Xcsr.indices[1::2] - - # normal sparse toarray() will sum the duplicated edges - Xdense = Xcsr.toarray() - assert_array_almost_equal(Xdense[:, 1::2], - X[:, ::2] + X[:, 1::2]) - - # csgraph_to_dense chooses the minimum of each duplicated edge - Xdense = csgraph_to_dense(Xcsr) - assert_array_almost_equal(Xdense[:, 1::2], - np.minimum(X[:, ::2], X[:, 1::2])) diff --git a/tests/csgraph/test_flow.py b/tests/csgraph/test_flow.py deleted file mode 100644 index 8bb129a572..0000000000 --- a/tests/csgraph/test_flow.py +++ /dev/null @@ -1,201 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_equal -import pytest - -from scipy.sparse import csr_matrix, csc_matrix -from scipy.sparse.csgraph import maximum_flow -from scipy.sparse.csgraph._flow import ( - _add_reverse_edges, _make_edge_pointers, _make_tails -) - -methods = ['edmonds_karp', 'dinic'] - -def test_raises_on_dense_input(): - with pytest.raises(TypeError): - graph = np.array([[0, 1], [0, 0]]) - maximum_flow(graph, 0, 1) - maximum_flow(graph, 0, 1, method='edmonds_karp') - - -def test_raises_on_csc_input(): - with pytest.raises(TypeError): - graph = csc_matrix([[0, 1], [0, 0]]) - maximum_flow(graph, 0, 1) - maximum_flow(graph, 0, 1, method='edmonds_karp') - - -def test_raises_on_floating_point_input(): - with pytest.raises(ValueError): - graph = csr_matrix([[0, 1.5], [0, 0]], dtype=np.float64) - maximum_flow(graph, 0, 1) - maximum_flow(graph, 0, 1, method='edmonds_karp') - - -def test_raises_on_non_square_input(): - with pytest.raises(ValueError): - graph = csr_matrix([[0, 1, 2], [2, 1, 0]]) - maximum_flow(graph, 0, 1) - - -def test_raises_when_source_is_sink(): - with pytest.raises(ValueError): - graph = csr_matrix([[0, 1], [0, 0]]) - maximum_flow(graph, 0, 0) - maximum_flow(graph, 0, 0, method='edmonds_karp') - - -@pytest.mark.parametrize('method', methods) -@pytest.mark.parametrize('source', [-1, 2, 3]) -def test_raises_when_source_is_out_of_bounds(source, method): - with pytest.raises(ValueError): - graph = csr_matrix([[0, 1], [0, 0]]) - maximum_flow(graph, source, 1, method=method) - - -@pytest.mark.parametrize('method', methods) -@pytest.mark.parametrize('sink', [-1, 2, 3]) -def test_raises_when_sink_is_out_of_bounds(sink, method): - with pytest.raises(ValueError): - graph = csr_matrix([[0, 1], [0, 0]]) - maximum_flow(graph, 0, sink, method=method) - - -@pytest.mark.parametrize('method', methods) -def test_simple_graph(method): - # This graph looks as follows: - # (0) --5--> (1) - graph = csr_matrix([[0, 5], [0, 0]]) - res = maximum_flow(graph, 0, 1, method=method) - assert res.flow_value == 5 - expected_flow = np.array([[0, 5], [-5, 0]]) - assert_array_equal(res.flow.toarray(), expected_flow) - - -@pytest.mark.parametrize('method', methods) -def test_bottle_neck_graph(method): - # This graph cannot use the full capacity between 0 and 1: - # (0) --5--> (1) --3--> (2) - graph = csr_matrix([[0, 5, 0], [0, 0, 3], [0, 0, 0]]) - res = maximum_flow(graph, 0, 2, method=method) - assert res.flow_value == 3 - expected_flow = np.array([[0, 3, 0], [-3, 0, 3], [0, -3, 0]]) - assert_array_equal(res.flow.toarray(), expected_flow) - - -@pytest.mark.parametrize('method', methods) -def test_backwards_flow(method): - # This example causes backwards flow between vertices 3 and 4, - # and so this test ensures that we handle that accordingly. See - # https://stackoverflow.com/q/38843963/5085211 - # for more information. - graph = csr_matrix([[0, 10, 0, 0, 10, 0, 0, 0], - [0, 0, 10, 0, 0, 0, 0, 0], - [0, 0, 0, 10, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 10], - [0, 0, 0, 10, 0, 10, 0, 0], - [0, 0, 0, 0, 0, 0, 10, 0], - [0, 0, 0, 0, 0, 0, 0, 10], - [0, 0, 0, 0, 0, 0, 0, 0]]) - res = maximum_flow(graph, 0, 7, method=method) - assert res.flow_value == 20 - expected_flow = np.array([[0, 10, 0, 0, 10, 0, 0, 0], - [-10, 0, 10, 0, 0, 0, 0, 0], - [0, -10, 0, 10, 0, 0, 0, 0], - [0, 0, -10, 0, 0, 0, 0, 10], - [-10, 0, 0, 0, 0, 10, 0, 0], - [0, 0, 0, 0, -10, 0, 10, 0], - [0, 0, 0, 0, 0, -10, 0, 10], - [0, 0, 0, -10, 0, 0, -10, 0]]) - assert_array_equal(res.flow.toarray(), expected_flow) - - -@pytest.mark.parametrize('method', methods) -def test_example_from_clrs_chapter_26_1(method): - # See page 659 in CLRS second edition, but note that the maximum flow - # we find is slightly different than the one in CLRS; we push a flow of - # 12 to v_1 instead of v_2. - graph = csr_matrix([[0, 16, 13, 0, 0, 0], - [0, 0, 10, 12, 0, 0], - [0, 4, 0, 0, 14, 0], - [0, 0, 9, 0, 0, 20], - [0, 0, 0, 7, 0, 4], - [0, 0, 0, 0, 0, 0]]) - res = maximum_flow(graph, 0, 5, method=method) - assert res.flow_value == 23 - expected_flow = np.array([[0, 12, 11, 0, 0, 0], - [-12, 0, 0, 12, 0, 0], - [-11, 0, 0, 0, 11, 0], - [0, -12, 0, 0, -7, 19], - [0, 0, -11, 7, 0, 4], - [0, 0, 0, -19, -4, 0]]) - assert_array_equal(res.flow.toarray(), expected_flow) - - -@pytest.mark.parametrize('method', methods) -def test_disconnected_graph(method): - # This tests the following disconnected graph: - # (0) --5--> (1) (2) --3--> (3) - graph = csr_matrix([[0, 5, 0, 0], - [0, 0, 0, 0], - [0, 0, 9, 3], - [0, 0, 0, 0]]) - res = maximum_flow(graph, 0, 3, method=method) - assert res.flow_value == 0 - expected_flow = np.zeros((4, 4), dtype=np.int32) - assert_array_equal(res.flow.toarray(), expected_flow) - - -@pytest.mark.parametrize('method', methods) -def test_add_reverse_edges_large_graph(method): - # Regression test for https://github.com/scipy/scipy/issues/14385 - n = 100_000 - indices = np.arange(1, n) - indptr = np.array(list(range(n)) + [n - 1]) - data = np.ones(n - 1, dtype=np.int32) - graph = csr_matrix((data, indices, indptr), shape=(n, n)) - res = maximum_flow(graph, 0, n - 1, method=method) - assert res.flow_value == 1 - expected_flow = graph - graph.transpose() - assert_array_equal(res.flow.data, expected_flow.data) - assert_array_equal(res.flow.indices, expected_flow.indices) - assert_array_equal(res.flow.indptr, expected_flow.indptr) - - -@pytest.mark.parametrize("a,b_data_expected", [ - ([[]], []), - ([[0], [0]], []), - ([[1, 0, 2], [0, 0, 0], [0, 3, 0]], [1, 2, 0, 0, 3]), - ([[9, 8, 7], [4, 5, 6], [0, 0, 0]], [9, 8, 7, 4, 5, 6, 0, 0])]) -def test_add_reverse_edges(a, b_data_expected): - """Test that the reversal of the edges of the input graph works - as expected. - """ - a = csr_matrix(a, dtype=np.int32, shape=(len(a), len(a))) - b = _add_reverse_edges(a) - assert_array_equal(b.data, b_data_expected) - - -@pytest.mark.parametrize("a,expected", [ - ([[]], []), - ([[0]], []), - ([[1]], [0]), - ([[0, 1], [10, 0]], [1, 0]), - ([[1, 0, 2], [0, 0, 3], [4, 5, 0]], [0, 3, 4, 1, 2]) -]) -def test_make_edge_pointers(a, expected): - a = csr_matrix(a, dtype=np.int32) - rev_edge_ptr = _make_edge_pointers(a) - assert_array_equal(rev_edge_ptr, expected) - - -@pytest.mark.parametrize("a,expected", [ - ([[]], []), - ([[0]], []), - ([[1]], [0]), - ([[0, 1], [10, 0]], [0, 1]), - ([[1, 0, 2], [0, 0, 3], [4, 5, 0]], [0, 0, 1, 2, 2]) -]) -def test_make_tails(a, expected): - a = csr_matrix(a, dtype=np.int32) - tails = _make_tails(a) - assert_array_equal(tails, expected) diff --git a/tests/csgraph/test_graph_laplacian.py b/tests/csgraph/test_graph_laplacian.py deleted file mode 100644 index 88805db0c9..0000000000 --- a/tests/csgraph/test_graph_laplacian.py +++ /dev/null @@ -1,368 +0,0 @@ -import pytest -import numpy as np -from numpy.testing import assert_allclose -from pytest import raises as assert_raises -from scipy import sparse - -from scipy.sparse import csgraph -from scipy._lib._util import np_long, np_ulong - - -def check_int_type(mat): - return np.issubdtype(mat.dtype, np.signedinteger) or np.issubdtype( - mat.dtype, np_ulong - ) - - -def test_laplacian_value_error(): - for t in int, float, complex: - for m in ([1, 1], - [[[1]]], - [[1, 2, 3], [4, 5, 6]], - [[1, 2], [3, 4], [5, 5]]): - A = np.array(m, dtype=t) - assert_raises(ValueError, csgraph.laplacian, A) - - -def _explicit_laplacian(x, normed=False): - if sparse.issparse(x): - x = x.toarray() - x = np.asarray(x) - y = -1.0 * x - for j in range(y.shape[0]): - y[j,j] = x[j,j+1:].sum() + x[j,:j].sum() - if normed: - d = np.diag(y).copy() - d[d == 0] = 1.0 - y /= d[:,None]**.5 - y /= d[None,:]**.5 - return y - - -def _check_symmetric_graph_laplacian(mat, normed, copy=True): - if not hasattr(mat, 'shape'): - mat = eval(mat, dict(np=np, sparse=sparse)) - - if sparse.issparse(mat): - sp_mat = mat - mat = sp_mat.toarray() - else: - sp_mat = sparse.csr_matrix(mat) - - mat_copy = np.copy(mat) - sp_mat_copy = sparse.csr_matrix(sp_mat, copy=True) - - n_nodes = mat.shape[0] - explicit_laplacian = _explicit_laplacian(mat, normed=normed) - laplacian = csgraph.laplacian(mat, normed=normed, copy=copy) - sp_laplacian = csgraph.laplacian(sp_mat, normed=normed, - copy=copy) - - if copy: - assert_allclose(mat, mat_copy) - _assert_allclose_sparse(sp_mat, sp_mat_copy) - else: - if not (normed and check_int_type(mat)): - assert_allclose(laplacian, mat) - if sp_mat.format == 'coo': - _assert_allclose_sparse(sp_laplacian, sp_mat) - - assert_allclose(laplacian, sp_laplacian.toarray()) - - for tested in [laplacian, sp_laplacian.toarray()]: - if not normed: - assert_allclose(tested.sum(axis=0), np.zeros(n_nodes)) - assert_allclose(tested.T, tested) - assert_allclose(tested, explicit_laplacian) - - -def test_symmetric_graph_laplacian(): - symmetric_mats = ( - 'np.arange(10) * np.arange(10)[:, np.newaxis]', - 'np.ones((7, 7))', - 'np.eye(19)', - 'sparse.diags([1, 1], [-1, 1], shape=(4, 4))', - 'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).toarray()', - 'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).todense()', - 'np.vander(np.arange(4)) + np.vander(np.arange(4)).T' - ) - for mat in symmetric_mats: - for normed in True, False: - for copy in True, False: - _check_symmetric_graph_laplacian(mat, normed, copy) - - -def _assert_allclose_sparse(a, b, **kwargs): - # helper function that can deal with sparse matrices - if sparse.issparse(a): - a = a.toarray() - if sparse.issparse(b): - b = b.toarray() - assert_allclose(a, b, **kwargs) - - -def _check_laplacian_dtype_none( - A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type -): - mat = arr_type(A, dtype=dtype) - L, d = csgraph.laplacian( - mat, - normed=normed, - return_diag=True, - use_out_degree=use_out_degree, - copy=copy, - dtype=None, - ) - if normed and check_int_type(mat): - assert L.dtype == np.float64 - assert d.dtype == np.float64 - _assert_allclose_sparse(L, desired_L, atol=1e-12) - _assert_allclose_sparse(d, desired_d, atol=1e-12) - else: - assert L.dtype == dtype - assert d.dtype == dtype - desired_L = np.asarray(desired_L).astype(dtype) - desired_d = np.asarray(desired_d).astype(dtype) - _assert_allclose_sparse(L, desired_L, atol=1e-12) - _assert_allclose_sparse(d, desired_d, atol=1e-12) - - if not copy: - if not (normed and check_int_type(mat)): - if type(mat) is np.ndarray: - assert_allclose(L, mat) - elif mat.format == "coo": - _assert_allclose_sparse(L, mat) - - -def _check_laplacian_dtype( - A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type -): - mat = arr_type(A, dtype=dtype) - L, d = csgraph.laplacian( - mat, - normed=normed, - return_diag=True, - use_out_degree=use_out_degree, - copy=copy, - dtype=dtype, - ) - assert L.dtype == dtype - assert d.dtype == dtype - desired_L = np.asarray(desired_L).astype(dtype) - desired_d = np.asarray(desired_d).astype(dtype) - _assert_allclose_sparse(L, desired_L, atol=1e-12) - _assert_allclose_sparse(d, desired_d, atol=1e-12) - - if not copy: - if not (normed and check_int_type(mat)): - if type(mat) is np.ndarray: - assert_allclose(L, mat) - elif mat.format == 'coo': - _assert_allclose_sparse(L, mat) - - -INT_DTYPES = (np.intc, np_long, np.longlong) -REAL_DTYPES = (np.float32, np.float64, np.longdouble) -COMPLEX_DTYPES = (np.complex64, np.complex128, np.clongdouble) -DTYPES = INT_DTYPES + REAL_DTYPES + COMPLEX_DTYPES - - -@pytest.mark.parametrize("dtype", DTYPES) -@pytest.mark.parametrize("arr_type", [np.array, - sparse.csr_matrix, - sparse.coo_matrix, - sparse.csr_array, - sparse.coo_array]) -@pytest.mark.parametrize("copy", [True, False]) -@pytest.mark.parametrize("normed", [True, False]) -@pytest.mark.parametrize("use_out_degree", [True, False]) -def test_asymmetric_laplacian(use_out_degree, normed, - copy, dtype, arr_type): - # adjacency matrix - A = [[0, 1, 0], - [4, 2, 0], - [0, 0, 0]] - A = arr_type(np.array(A), dtype=dtype) - A_copy = A.copy() - - if not normed and use_out_degree: - # Laplacian matrix using out-degree - L = [[1, -1, 0], - [-4, 4, 0], - [0, 0, 0]] - d = [1, 4, 0] - - if normed and use_out_degree: - # normalized Laplacian matrix using out-degree - L = [[1, -0.5, 0], - [-2, 1, 0], - [0, 0, 0]] - d = [1, 2, 1] - - if not normed and not use_out_degree: - # Laplacian matrix using in-degree - L = [[4, -1, 0], - [-4, 1, 0], - [0, 0, 0]] - d = [4, 1, 0] - - if normed and not use_out_degree: - # normalized Laplacian matrix using in-degree - L = [[1, -0.5, 0], - [-2, 1, 0], - [0, 0, 0]] - d = [2, 1, 1] - - _check_laplacian_dtype_none( - A, - L, - d, - normed=normed, - use_out_degree=use_out_degree, - copy=copy, - dtype=dtype, - arr_type=arr_type, - ) - - _check_laplacian_dtype( - A_copy, - L, - d, - normed=normed, - use_out_degree=use_out_degree, - copy=copy, - dtype=dtype, - arr_type=arr_type, - ) - - -@pytest.mark.parametrize("fmt", ['csr', 'csc', 'coo', 'lil', - 'dok', 'dia', 'bsr']) -@pytest.mark.parametrize("normed", [True, False]) -@pytest.mark.parametrize("copy", [True, False]) -def test_sparse_formats(fmt, normed, copy): - mat = sparse.diags([1, 1], [-1, 1], shape=(4, 4), format=fmt) - _check_symmetric_graph_laplacian(mat, normed, copy) - - -@pytest.mark.parametrize( - "arr_type", [np.asarray, - sparse.csr_matrix, - sparse.coo_matrix, - sparse.csr_array, - sparse.coo_array] -) -@pytest.mark.parametrize("form", ["array", "function", "lo"]) -def test_laplacian_symmetrized(arr_type, form): - # adjacency matrix - n = 3 - mat = arr_type(np.arange(n * n).reshape(n, n)) - L_in, d_in = csgraph.laplacian( - mat, - return_diag=True, - form=form, - ) - L_out, d_out = csgraph.laplacian( - mat, - return_diag=True, - use_out_degree=True, - form=form, - ) - Ls, ds = csgraph.laplacian( - mat, - return_diag=True, - symmetrized=True, - form=form, - ) - Ls_normed, ds_normed = csgraph.laplacian( - mat, - return_diag=True, - symmetrized=True, - normed=True, - form=form, - ) - mat += mat.T - Lss, dss = csgraph.laplacian(mat, return_diag=True, form=form) - Lss_normed, dss_normed = csgraph.laplacian( - mat, - return_diag=True, - normed=True, - form=form, - ) - - assert_allclose(ds, d_in + d_out) - assert_allclose(ds, dss) - assert_allclose(ds_normed, dss_normed) - - d = {} - for L in ["L_in", "L_out", "Ls", "Ls_normed", "Lss", "Lss_normed"]: - if form == "array": - d[L] = eval(L) - else: - d[L] = eval(L)(np.eye(n, dtype=mat.dtype)) - - _assert_allclose_sparse(d["Ls"], d["L_in"] + d["L_out"].T) - _assert_allclose_sparse(d["Ls"], d["Lss"]) - _assert_allclose_sparse(d["Ls_normed"], d["Lss_normed"]) - - -@pytest.mark.parametrize( - "arr_type", [np.asarray, - sparse.csr_matrix, - sparse.coo_matrix, - sparse.csr_array, - sparse.coo_array] -) -@pytest.mark.parametrize("dtype", DTYPES) -@pytest.mark.parametrize("normed", [True, False]) -@pytest.mark.parametrize("symmetrized", [True, False]) -@pytest.mark.parametrize("use_out_degree", [True, False]) -@pytest.mark.parametrize("form", ["function", "lo"]) -def test_format(dtype, arr_type, normed, symmetrized, use_out_degree, form): - n = 3 - mat = [[0, 1, 0], [4, 2, 0], [0, 0, 0]] - mat = arr_type(np.array(mat), dtype=dtype) - Lo, do = csgraph.laplacian( - mat, - return_diag=True, - normed=normed, - symmetrized=symmetrized, - use_out_degree=use_out_degree, - dtype=dtype, - ) - La, da = csgraph.laplacian( - mat, - return_diag=True, - normed=normed, - symmetrized=symmetrized, - use_out_degree=use_out_degree, - dtype=dtype, - form="array", - ) - assert_allclose(do, da) - _assert_allclose_sparse(Lo, La) - - L, d = csgraph.laplacian( - mat, - return_diag=True, - normed=normed, - symmetrized=symmetrized, - use_out_degree=use_out_degree, - dtype=dtype, - form=form, - ) - assert_allclose(d, do) - assert d.dtype == dtype - Lm = L(np.eye(n, dtype=mat.dtype)).astype(dtype) - _assert_allclose_sparse(Lm, Lo, rtol=2e-7, atol=2e-7) - x = np.arange(6).reshape(3, 2) - if not (normed and dtype in INT_DTYPES): - assert_allclose(L(x), Lo @ x) - else: - # Normalized Lo is casted to integer, but L() is not - pass - - -def test_format_error_message(): - with pytest.raises(ValueError, match="Invalid form: 'toto'"): - _ = csgraph.laplacian(np.eye(1), form='toto') diff --git a/tests/csgraph/test_matching.py b/tests/csgraph/test_matching.py deleted file mode 100644 index 87e2920fe9..0000000000 --- a/tests/csgraph/test_matching.py +++ /dev/null @@ -1,294 +0,0 @@ -from itertools import product - -import numpy as np -from numpy.testing import assert_array_equal, assert_equal -import pytest - -from scipy.sparse import csr_matrix, coo_matrix, diags -from scipy.sparse.csgraph import ( - maximum_bipartite_matching, min_weight_full_bipartite_matching -) - - -def test_maximum_bipartite_matching_raises_on_dense_input(): - with pytest.raises(TypeError): - graph = np.array([[0, 1], [0, 0]]) - maximum_bipartite_matching(graph) - - -def test_maximum_bipartite_matching_empty_graph(): - graph = csr_matrix((0, 0)) - x = maximum_bipartite_matching(graph, perm_type='row') - y = maximum_bipartite_matching(graph, perm_type='column') - expected_matching = np.array([]) - assert_array_equal(expected_matching, x) - assert_array_equal(expected_matching, y) - - -def test_maximum_bipartite_matching_empty_left_partition(): - graph = csr_matrix((2, 0)) - x = maximum_bipartite_matching(graph, perm_type='row') - y = maximum_bipartite_matching(graph, perm_type='column') - assert_array_equal(np.array([]), x) - assert_array_equal(np.array([-1, -1]), y) - - -def test_maximum_bipartite_matching_empty_right_partition(): - graph = csr_matrix((0, 3)) - x = maximum_bipartite_matching(graph, perm_type='row') - y = maximum_bipartite_matching(graph, perm_type='column') - assert_array_equal(np.array([-1, -1, -1]), x) - assert_array_equal(np.array([]), y) - - -def test_maximum_bipartite_matching_graph_with_no_edges(): - graph = csr_matrix((2, 2)) - x = maximum_bipartite_matching(graph, perm_type='row') - y = maximum_bipartite_matching(graph, perm_type='column') - assert_array_equal(np.array([-1, -1]), x) - assert_array_equal(np.array([-1, -1]), y) - - -def test_maximum_bipartite_matching_graph_that_causes_augmentation(): - # In this graph, column 1 is initially assigned to row 1, but it should be - # reassigned to make room for row 2. - graph = csr_matrix([[1, 1], [1, 0]]) - x = maximum_bipartite_matching(graph, perm_type='column') - y = maximum_bipartite_matching(graph, perm_type='row') - expected_matching = np.array([1, 0]) - assert_array_equal(expected_matching, x) - assert_array_equal(expected_matching, y) - - -def test_maximum_bipartite_matching_graph_with_more_rows_than_columns(): - graph = csr_matrix([[1, 1], [1, 0], [0, 1]]) - x = maximum_bipartite_matching(graph, perm_type='column') - y = maximum_bipartite_matching(graph, perm_type='row') - assert_array_equal(np.array([0, -1, 1]), x) - assert_array_equal(np.array([0, 2]), y) - - -def test_maximum_bipartite_matching_graph_with_more_columns_than_rows(): - graph = csr_matrix([[1, 1, 0], [0, 0, 1]]) - x = maximum_bipartite_matching(graph, perm_type='column') - y = maximum_bipartite_matching(graph, perm_type='row') - assert_array_equal(np.array([0, 2]), x) - assert_array_equal(np.array([0, -1, 1]), y) - - -def test_maximum_bipartite_matching_explicit_zeros_count_as_edges(): - data = [0, 0] - indices = [1, 0] - indptr = [0, 1, 2] - graph = csr_matrix((data, indices, indptr), shape=(2, 2)) - x = maximum_bipartite_matching(graph, perm_type='row') - y = maximum_bipartite_matching(graph, perm_type='column') - expected_matching = np.array([1, 0]) - assert_array_equal(expected_matching, x) - assert_array_equal(expected_matching, y) - - -def test_maximum_bipartite_matching_feasibility_of_result(): - # This is a regression test for GitHub issue #11458 - data = np.ones(50, dtype=int) - indices = [11, 12, 19, 22, 23, 5, 22, 3, 8, 10, 5, 6, 11, 12, 13, 5, 13, - 14, 20, 22, 3, 15, 3, 13, 14, 11, 12, 19, 22, 23, 5, 22, 3, 8, - 10, 5, 6, 11, 12, 13, 5, 13, 14, 20, 22, 3, 15, 3, 13, 14] - indptr = [0, 5, 7, 10, 10, 15, 20, 22, 22, 23, 25, 30, 32, 35, 35, 40, 45, - 47, 47, 48, 50] - graph = csr_matrix((data, indices, indptr), shape=(20, 25)) - x = maximum_bipartite_matching(graph, perm_type='row') - y = maximum_bipartite_matching(graph, perm_type='column') - assert (x != -1).sum() == 13 - assert (y != -1).sum() == 13 - # Ensure that each element of the matching is in fact an edge in the graph. - for u, v in zip(range(graph.shape[0]), y): - if v != -1: - assert graph[u, v] - for u, v in zip(x, range(graph.shape[1])): - if u != -1: - assert graph[u, v] - - -def test_matching_large_random_graph_with_one_edge_incident_to_each_vertex(): - np.random.seed(42) - A = diags(np.ones(25), offsets=0, format='csr') - rand_perm = np.random.permutation(25) - rand_perm2 = np.random.permutation(25) - - Rrow = np.arange(25) - Rcol = rand_perm - Rdata = np.ones(25, dtype=int) - Rmat = coo_matrix((Rdata, (Rrow, Rcol))).tocsr() - - Crow = rand_perm2 - Ccol = np.arange(25) - Cdata = np.ones(25, dtype=int) - Cmat = coo_matrix((Cdata, (Crow, Ccol))).tocsr() - # Randomly permute identity matrix - B = Rmat * A * Cmat - - # Row permute - perm = maximum_bipartite_matching(B, perm_type='row') - Rrow = np.arange(25) - Rcol = perm - Rdata = np.ones(25, dtype=int) - Rmat = coo_matrix((Rdata, (Rrow, Rcol))).tocsr() - C1 = Rmat * B - - # Column permute - perm2 = maximum_bipartite_matching(B, perm_type='column') - Crow = perm2 - Ccol = np.arange(25) - Cdata = np.ones(25, dtype=int) - Cmat = coo_matrix((Cdata, (Crow, Ccol))).tocsr() - C2 = B * Cmat - - # Should get identity matrix back - assert_equal(any(C1.diagonal() == 0), False) - assert_equal(any(C2.diagonal() == 0), False) - - -@pytest.mark.parametrize('num_rows,num_cols', [(0, 0), (2, 0), (0, 3)]) -def test_min_weight_full_matching_trivial_graph(num_rows, num_cols): - biadjacency_matrix = csr_matrix((num_cols, num_rows)) - row_ind, col_ind = min_weight_full_bipartite_matching(biadjacency_matrix) - assert len(row_ind) == 0 - assert len(col_ind) == 0 - - -@pytest.mark.parametrize('biadjacency_matrix', - [ - [[1, 1, 1], [1, 0, 0], [1, 0, 0]], - [[1, 1, 1], [0, 0, 1], [0, 0, 1]], - [[1, 0, 0, 1], [1, 1, 0, 1], [0, 0, 0, 0]], - [[1, 0, 0], [2, 0, 0]], - [[0, 1, 0], [0, 2, 0]], - [[1, 0], [2, 0], [5, 0]] - ]) -def test_min_weight_full_matching_infeasible_problems(biadjacency_matrix): - with pytest.raises(ValueError): - min_weight_full_bipartite_matching(csr_matrix(biadjacency_matrix)) - - -def test_min_weight_full_matching_large_infeasible(): - # Regression test for GitHub issue #17269 - a = np.asarray([ - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001], - [0.0, 0.11687445, 0.0, 0.0, 0.01319788, 0.07509257, 0.0, - 0.0, 0.0, 0.74228317, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.81087935, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.8408466, 0.0, 0.0, 0.0, 0.0, 0.01194389, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.82994211, 0.0, 0.0, 0.0, 0.11468516, 0.0, 0.0, 0.0, - 0.11173505, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0], - [0.18796507, 0.0, 0.04002318, 0.0, 0.0, 0.0, 0.0, 0.0, 0.75883335, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.71545464, 0.0, 0.0, 0.0, 0.0, 0.0, 0.02748488, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.78470564, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.14829198, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.10870609, 0.0, 0.0, 0.0, 0.8918677, 0.0, 0.0, 0.0, 0.06306644, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.63844085, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7442354, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09850549, 0.0, 0.0, 0.18638258, - 0.2769244, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.73182464, 0.0, 0.0, 0.46443561, - 0.38589284, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.29510278, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09666032, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - ]) - with pytest.raises(ValueError, match='no full matching exists'): - min_weight_full_bipartite_matching(csr_matrix(a)) - - -def test_explicit_zero_causes_warning(): - with pytest.warns(UserWarning): - biadjacency_matrix = csr_matrix(((2, 0, 3), (0, 1, 1), (0, 2, 3))) - min_weight_full_bipartite_matching(biadjacency_matrix) - - -# General test for linear sum assignment solvers to make it possible to rely -# on the same tests for scipy.optimize.linear_sum_assignment. -def linear_sum_assignment_assertions( - solver, array_type, sign, test_case -): - cost_matrix, expected_cost = test_case - maximize = sign == -1 - cost_matrix = sign * array_type(cost_matrix) - expected_cost = sign * np.array(expected_cost) - - row_ind, col_ind = solver(cost_matrix, maximize=maximize) - assert_array_equal(row_ind, np.sort(row_ind)) - assert_array_equal(expected_cost, - np.array(cost_matrix[row_ind, col_ind]).flatten()) - - cost_matrix = cost_matrix.T - row_ind, col_ind = solver(cost_matrix, maximize=maximize) - assert_array_equal(row_ind, np.sort(row_ind)) - assert_array_equal(np.sort(expected_cost), - np.sort(np.array( - cost_matrix[row_ind, col_ind])).flatten()) - - -linear_sum_assignment_test_cases = product( - [-1, 1], - [ - # Square - ([[400, 150, 400], - [400, 450, 600], - [300, 225, 300]], - [150, 400, 300]), - - # Rectangular variant - ([[400, 150, 400, 1], - [400, 450, 600, 2], - [300, 225, 300, 3]], - [150, 2, 300]), - - ([[10, 10, 8], - [9, 8, 1], - [9, 7, 4]], - [10, 1, 7]), - - # Square - ([[10, 10, 8, 11], - [9, 8, 1, 1], - [9, 7, 4, 10]], - [10, 1, 4]), - - # Rectangular variant - ([[10, float("inf"), float("inf")], - [float("inf"), float("inf"), 1], - [float("inf"), 7, float("inf")]], - [10, 1, 7]) - ]) - - -@pytest.mark.parametrize('sign,test_case', linear_sum_assignment_test_cases) -def test_min_weight_full_matching_small_inputs(sign, test_case): - linear_sum_assignment_assertions( - min_weight_full_bipartite_matching, csr_matrix, sign, test_case) diff --git a/tests/csgraph/test_pydata_sparse.py b/tests/csgraph/test_pydata_sparse.py deleted file mode 100644 index 025aeb67c1..0000000000 --- a/tests/csgraph/test_pydata_sparse.py +++ /dev/null @@ -1,194 +0,0 @@ -import pytest - -import numpy as np -import scipy.sparse as sp -import scipy.sparse.csgraph as spgraph -from scipy._lib import _pep440 - -from numpy.testing import assert_equal - -try: - import sparse -except Exception: - sparse = None - -pytestmark = pytest.mark.skipif(sparse is None, - reason="pydata/sparse not installed") - - -msg = "pydata/sparse (0.15.1) does not implement necessary operations" - - -sparse_params = (pytest.param("COO"), - pytest.param("DOK", marks=[pytest.mark.xfail(reason=msg)])) - - -def check_sparse_version(min_ver): - if sparse is None: - return pytest.mark.skip(reason="sparse is not installed") - return pytest.mark.skipif( - _pep440.parse(sparse.__version__) < _pep440.Version(min_ver), - reason=f"sparse version >= {min_ver} required" - ) - - -@pytest.fixture(params=sparse_params) -def sparse_cls(request): - return getattr(sparse, request.param) - - -@pytest.fixture -def graphs(sparse_cls): - graph = [ - [0, 1, 1, 0, 0], - [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 1], - [0, 0, 0, 0, 0], - ] - A_dense = np.array(graph) - A_sparse = sparse_cls(A_dense) - return A_dense, A_sparse - - -@pytest.mark.parametrize( - "func", - [ - spgraph.shortest_path, - spgraph.dijkstra, - spgraph.floyd_warshall, - spgraph.bellman_ford, - spgraph.johnson, - spgraph.reverse_cuthill_mckee, - spgraph.maximum_bipartite_matching, - spgraph.structural_rank, - ] -) -def test_csgraph_equiv(func, graphs): - A_dense, A_sparse = graphs - actual = func(A_sparse) - desired = func(sp.csc_matrix(A_dense)) - assert_equal(actual, desired) - - -def test_connected_components(graphs): - A_dense, A_sparse = graphs - func = spgraph.connected_components - - actual_comp, actual_labels = func(A_sparse) - desired_comp, desired_labels, = func(sp.csc_matrix(A_dense)) - - assert actual_comp == desired_comp - assert_equal(actual_labels, desired_labels) - - -def test_laplacian(graphs): - A_dense, A_sparse = graphs - sparse_cls = type(A_sparse) - func = spgraph.laplacian - - actual = func(A_sparse) - desired = func(sp.csc_matrix(A_dense)) - - assert isinstance(actual, sparse_cls) - - assert_equal(actual.todense(), desired.todense()) - - -@pytest.mark.parametrize( - "func", [spgraph.breadth_first_order, spgraph.depth_first_order] -) -def test_order_search(graphs, func): - A_dense, A_sparse = graphs - - actual = func(A_sparse, 0) - desired = func(sp.csc_matrix(A_dense), 0) - - assert_equal(actual, desired) - - -@pytest.mark.parametrize( - "func", [spgraph.breadth_first_tree, spgraph.depth_first_tree] -) -def test_tree_search(graphs, func): - A_dense, A_sparse = graphs - sparse_cls = type(A_sparse) - - actual = func(A_sparse, 0) - desired = func(sp.csc_matrix(A_dense), 0) - - assert isinstance(actual, sparse_cls) - - assert_equal(actual.todense(), desired.todense()) - - -def test_minimum_spanning_tree(graphs): - A_dense, A_sparse = graphs - sparse_cls = type(A_sparse) - func = spgraph.minimum_spanning_tree - - actual = func(A_sparse) - desired = func(sp.csc_matrix(A_dense)) - - assert isinstance(actual, sparse_cls) - - assert_equal(actual.todense(), desired.todense()) - - -def test_maximum_flow(graphs): - A_dense, A_sparse = graphs - sparse_cls = type(A_sparse) - func = spgraph.maximum_flow - - actual = func(A_sparse, 0, 2) - desired = func(sp.csr_matrix(A_dense), 0, 2) - - assert actual.flow_value == desired.flow_value - assert isinstance(actual.flow, sparse_cls) - - assert_equal(actual.flow.todense(), desired.flow.todense()) - - -def test_min_weight_full_bipartite_matching(graphs): - A_dense, A_sparse = graphs - func = spgraph.min_weight_full_bipartite_matching - - actual = func(A_sparse[0:2, 1:3]) - desired = func(sp.csc_matrix(A_dense)[0:2, 1:3]) - - assert_equal(actual, desired) - - -@check_sparse_version("0.15.4") -@pytest.mark.parametrize( - "func", - [ - spgraph.shortest_path, - spgraph.dijkstra, - spgraph.floyd_warshall, - spgraph.bellman_ford, - spgraph.johnson, - spgraph.minimum_spanning_tree, - ] -) -@pytest.mark.parametrize( - "fill_value, comp_func", - [(np.inf, np.isposinf), (np.nan, np.isnan)], -) -def test_nonzero_fill_value(graphs, func, fill_value, comp_func): - A_dense, A_sparse = graphs - A_sparse = A_sparse.astype(float) - A_sparse.fill_value = fill_value - sparse_cls = type(A_sparse) - - actual = func(A_sparse) - desired = func(sp.csc_matrix(A_dense)) - - if func == spgraph.minimum_spanning_tree: - assert isinstance(actual, sparse_cls) - assert comp_func(actual.fill_value) - actual = actual.todense() - actual[comp_func(actual)] = 0.0 - assert_equal(actual, desired.todense()) - else: - assert_equal(actual, desired) diff --git a/tests/csgraph/test_reordering.py b/tests/csgraph/test_reordering.py deleted file mode 100644 index cb4c002fa3..0000000000 --- a/tests/csgraph/test_reordering.py +++ /dev/null @@ -1,70 +0,0 @@ -import numpy as np -from numpy.testing import assert_equal -from scipy.sparse.csgraph import reverse_cuthill_mckee, structural_rank -from scipy.sparse import csc_matrix, csr_matrix, coo_matrix - - -def test_graph_reverse_cuthill_mckee(): - A = np.array([[1, 0, 0, 0, 1, 0, 0, 0], - [0, 1, 1, 0, 0, 1, 0, 1], - [0, 1, 1, 0, 1, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 1, 0], - [1, 0, 1, 0, 1, 0, 0, 0], - [0, 1, 0, 0, 0, 1, 0, 1], - [0, 0, 0, 1, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0, 1]], dtype=int) - - graph = csr_matrix(A) - perm = reverse_cuthill_mckee(graph) - correct_perm = np.array([6, 3, 7, 5, 1, 2, 4, 0]) - assert_equal(perm, correct_perm) - - # Test int64 indices input - graph.indices = graph.indices.astype('int64') - graph.indptr = graph.indptr.astype('int64') - perm = reverse_cuthill_mckee(graph, True) - assert_equal(perm, correct_perm) - - -def test_graph_reverse_cuthill_mckee_ordering(): - data = np.ones(63,dtype=int) - rows = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, - 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, - 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, - 9, 10, 10, 10, 10, 10, 11, 11, 11, 11, - 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, - 14, 15, 15, 15, 15, 15]) - cols = np.array([0, 2, 5, 8, 10, 1, 3, 9, 11, 0, 2, - 7, 10, 1, 3, 11, 4, 6, 12, 14, 0, 7, 13, - 15, 4, 6, 14, 2, 5, 7, 15, 0, 8, 10, 13, - 1, 9, 11, 0, 2, 8, 10, 15, 1, 3, 9, 11, - 4, 12, 14, 5, 8, 13, 15, 4, 6, 12, 14, - 5, 7, 10, 13, 15]) - graph = coo_matrix((data, (rows,cols))).tocsr() - perm = reverse_cuthill_mckee(graph) - correct_perm = np.array([12, 14, 4, 6, 10, 8, 2, 15, - 0, 13, 7, 5, 9, 11, 1, 3]) - assert_equal(perm, correct_perm) - - -def test_graph_structural_rank(): - # Test square matrix #1 - A = csc_matrix([[1, 1, 0], - [1, 0, 1], - [0, 1, 0]]) - assert_equal(structural_rank(A), 3) - - # Test square matrix #2 - rows = np.array([0,0,0,0,0,1,1,2,2,3,3,3,3,3,3,4,4,5,5,6,6,7,7]) - cols = np.array([0,1,2,3,4,2,5,2,6,0,1,3,5,6,7,4,5,5,6,2,6,2,4]) - data = np.ones_like(rows) - B = coo_matrix((data,(rows,cols)), shape=(8,8)) - assert_equal(structural_rank(B), 6) - - #Test non-square matrix - C = csc_matrix([[1, 0, 2, 0], - [2, 0, 4, 0]]) - assert_equal(structural_rank(C), 2) - - #Test tall matrix - assert_equal(structural_rank(C.T), 2) diff --git a/tests/csgraph/test_shortest_path.py b/tests/csgraph/test_shortest_path.py deleted file mode 100644 index 45600352e8..0000000000 --- a/tests/csgraph/test_shortest_path.py +++ /dev/null @@ -1,484 +0,0 @@ -from io import StringIO -import warnings -import numpy as np -from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_allclose -from pytest import raises as assert_raises -from scipy.sparse.csgraph import (shortest_path, dijkstra, johnson, - bellman_ford, construct_dist_matrix, yen, - NegativeCycleError) -import scipy.sparse -from scipy.io import mmread -import pytest - -directed_G = np.array([[0, 3, 3, 0, 0], - [0, 0, 0, 2, 4], - [0, 0, 0, 0, 0], - [1, 0, 0, 0, 0], - [2, 0, 0, 2, 0]], dtype=float) - -undirected_G = np.array([[0, 3, 3, 1, 2], - [3, 0, 0, 2, 4], - [3, 0, 0, 0, 0], - [1, 2, 0, 0, 2], - [2, 4, 0, 2, 0]], dtype=float) - -unweighted_G = (directed_G > 0).astype(float) - -directed_SP = [[0, 3, 3, 5, 7], - [3, 0, 6, 2, 4], - [np.inf, np.inf, 0, np.inf, np.inf], - [1, 4, 4, 0, 8], - [2, 5, 5, 2, 0]] - -directed_2SP_0_to_3 = [[-9999, 0, -9999, 1, -9999], - [-9999, 0, -9999, 4, 1]] - -directed_sparse_zero_G = scipy.sparse.csr_matrix( - ( - [0, 1, 2, 3, 1], - ([0, 1, 2, 3, 4], [1, 2, 0, 4, 3]), - ), - shape=(5, 5), -) - -directed_sparse_zero_SP = [[0, 0, 1, np.inf, np.inf], - [3, 0, 1, np.inf, np.inf], - [2, 2, 0, np.inf, np.inf], - [np.inf, np.inf, np.inf, 0, 3], - [np.inf, np.inf, np.inf, 1, 0]] - -undirected_sparse_zero_G = scipy.sparse.csr_matrix( - ( - [0, 0, 1, 1, 2, 2, 1, 1], - ([0, 1, 1, 2, 2, 0, 3, 4], [1, 0, 2, 1, 0, 2, 4, 3]) - ), - shape=(5, 5), -) - -undirected_sparse_zero_SP = [[0, 0, 1, np.inf, np.inf], - [0, 0, 1, np.inf, np.inf], - [1, 1, 0, np.inf, np.inf], - [np.inf, np.inf, np.inf, 0, 1], - [np.inf, np.inf, np.inf, 1, 0]] - -directed_pred = np.array([[-9999, 0, 0, 1, 1], - [3, -9999, 0, 1, 1], - [-9999, -9999, -9999, -9999, -9999], - [3, 0, 0, -9999, 1], - [4, 0, 0, 4, -9999]], dtype=float) - -undirected_SP = np.array([[0, 3, 3, 1, 2], - [3, 0, 6, 2, 4], - [3, 6, 0, 4, 5], - [1, 2, 4, 0, 2], - [2, 4, 5, 2, 0]], dtype=float) - -undirected_SP_limit_2 = np.array([[0, np.inf, np.inf, 1, 2], - [np.inf, 0, np.inf, 2, np.inf], - [np.inf, np.inf, 0, np.inf, np.inf], - [1, 2, np.inf, 0, 2], - [2, np.inf, np.inf, 2, 0]], dtype=float) - -undirected_SP_limit_0 = np.ones((5, 5), dtype=float) - np.eye(5) -undirected_SP_limit_0[undirected_SP_limit_0 > 0] = np.inf - -undirected_pred = np.array([[-9999, 0, 0, 0, 0], - [1, -9999, 0, 1, 1], - [2, 0, -9999, 0, 0], - [3, 3, 0, -9999, 3], - [4, 4, 0, 4, -9999]], dtype=float) - -directed_negative_weighted_G = np.array([[0, 0, 0], - [-1, 0, 0], - [0, -1, 0]], dtype=float) - -directed_negative_weighted_SP = np.array([[0, np.inf, np.inf], - [-1, 0, np.inf], - [-2, -1, 0]], dtype=float) - -methods = ['auto', 'FW', 'D', 'BF', 'J'] - - -def test_dijkstra_limit(): - limits = [0, 2, np.inf] - results = [undirected_SP_limit_0, - undirected_SP_limit_2, - undirected_SP] - - def check(limit, result): - SP = dijkstra(undirected_G, directed=False, limit=limit) - assert_array_almost_equal(SP, result) - - for limit, result in zip(limits, results): - check(limit, result) - - -def test_directed(): - def check(method): - SP = shortest_path(directed_G, method=method, directed=True, - overwrite=False) - assert_array_almost_equal(SP, directed_SP) - - for method in methods: - check(method) - - -def test_undirected(): - def check(method, directed_in): - if directed_in: - SP1 = shortest_path(directed_G, method=method, directed=False, - overwrite=False) - assert_array_almost_equal(SP1, undirected_SP) - else: - SP2 = shortest_path(undirected_G, method=method, directed=True, - overwrite=False) - assert_array_almost_equal(SP2, undirected_SP) - - for method in methods: - for directed_in in (True, False): - check(method, directed_in) - - -def test_directed_sparse_zero(): - # test directed sparse graph with zero-weight edge and two connected components - def check(method): - SP = shortest_path(directed_sparse_zero_G, method=method, directed=True, - overwrite=False) - assert_array_almost_equal(SP, directed_sparse_zero_SP) - - for method in methods: - check(method) - - -def test_undirected_sparse_zero(): - def check(method, directed_in): - if directed_in: - SP1 = shortest_path(directed_sparse_zero_G, method=method, directed=False, - overwrite=False) - assert_array_almost_equal(SP1, undirected_sparse_zero_SP) - else: - SP2 = shortest_path(undirected_sparse_zero_G, method=method, directed=True, - overwrite=False) - assert_array_almost_equal(SP2, undirected_sparse_zero_SP) - - for method in methods: - for directed_in in (True, False): - check(method, directed_in) - - -@pytest.mark.parametrize('directed, SP_ans', - ((True, directed_SP), - (False, undirected_SP))) -@pytest.mark.parametrize('indices', ([0, 2, 4], [0, 4], [3, 4], [0, 0])) -def test_dijkstra_indices_min_only(directed, SP_ans, indices): - SP_ans = np.array(SP_ans) - indices = np.array(indices, dtype=np.int64) - min_ind_ans = indices[np.argmin(SP_ans[indices, :], axis=0)] - min_d_ans = np.zeros(SP_ans.shape[0], SP_ans.dtype) - for k in range(SP_ans.shape[0]): - min_d_ans[k] = SP_ans[min_ind_ans[k], k] - min_ind_ans[np.isinf(min_d_ans)] = -9999 - - SP, pred, sources = dijkstra(directed_G, - directed=directed, - indices=indices, - min_only=True, - return_predecessors=True) - assert_array_almost_equal(SP, min_d_ans) - assert_array_equal(min_ind_ans, sources) - SP = dijkstra(directed_G, - directed=directed, - indices=indices, - min_only=True, - return_predecessors=False) - assert_array_almost_equal(SP, min_d_ans) - - -@pytest.mark.parametrize('n', (10, 100, 1000)) -def test_dijkstra_min_only_random(n): - np.random.seed(1234) - data = scipy.sparse.rand(n, n, density=0.5, format='lil', - random_state=42, dtype=np.float64) - data.setdiag(np.zeros(n, dtype=np.bool_)) - # choose some random vertices - v = np.arange(n) - np.random.shuffle(v) - indices = v[:int(n*.1)] - ds, pred, sources = dijkstra(data, - directed=True, - indices=indices, - min_only=True, - return_predecessors=True) - for k in range(n): - p = pred[k] - s = sources[k] - while p != -9999: - assert sources[p] == s - p = pred[p] - - -def test_dijkstra_random(): - # reproduces the hang observed in gh-17782 - n = 10 - indices = [0, 4, 4, 5, 7, 9, 0, 6, 2, 3, 7, 9, 1, 2, 9, 2, 5, 6] - indptr = [0, 0, 2, 5, 6, 7, 8, 12, 15, 18, 18] - data = [0.33629, 0.40458, 0.47493, 0.42757, 0.11497, 0.91653, 0.69084, - 0.64979, 0.62555, 0.743, 0.01724, 0.99945, 0.31095, 0.15557, - 0.02439, 0.65814, 0.23478, 0.24072] - graph = scipy.sparse.csr_matrix((data, indices, indptr), shape=(n, n)) - dijkstra(graph, directed=True, return_predecessors=True) - - -def test_gh_17782_segfault(): - text = """%%MatrixMarket matrix coordinate real general - 84 84 22 - 2 1 4.699999809265137e+00 - 6 14 1.199999973177910e-01 - 9 6 1.199999973177910e-01 - 10 16 2.012000083923340e+01 - 11 10 1.422000026702881e+01 - 12 1 9.645999908447266e+01 - 13 18 2.012000083923340e+01 - 14 13 4.679999828338623e+00 - 15 11 1.199999973177910e-01 - 16 12 1.199999973177910e-01 - 18 15 1.199999973177910e-01 - 32 2 2.299999952316284e+00 - 33 20 6.000000000000000e+00 - 33 32 5.000000000000000e+00 - 36 9 3.720000028610229e+00 - 36 37 3.720000028610229e+00 - 36 38 3.720000028610229e+00 - 37 44 8.159999847412109e+00 - 38 32 7.903999328613281e+01 - 43 20 2.400000000000000e+01 - 43 33 4.000000000000000e+00 - 44 43 6.028000259399414e+01 - """ - data = mmread(StringIO(text)) - dijkstra(data, directed=True, return_predecessors=True) - - -def test_shortest_path_indices(): - indices = np.arange(4) - - def check(func, indshape): - outshape = indshape + (5,) - SP = func(directed_G, directed=False, - indices=indices.reshape(indshape)) - assert_array_almost_equal(SP, undirected_SP[indices].reshape(outshape)) - - for indshape in [(4,), (4, 1), (2, 2)]: - for func in (dijkstra, bellman_ford, johnson, shortest_path): - check(func, indshape) - - assert_raises(ValueError, shortest_path, directed_G, method='FW', - indices=indices) - - -def test_predecessors(): - SP_res = {True: directed_SP, - False: undirected_SP} - pred_res = {True: directed_pred, - False: undirected_pred} - - def check(method, directed): - SP, pred = shortest_path(directed_G, method, directed=directed, - overwrite=False, - return_predecessors=True) - assert_array_almost_equal(SP, SP_res[directed]) - assert_array_almost_equal(pred, pred_res[directed]) - - for method in methods: - for directed in (True, False): - check(method, directed) - - -def test_construct_shortest_path(): - def check(method, directed): - SP1, pred = shortest_path(directed_G, - directed=directed, - overwrite=False, - return_predecessors=True) - SP2 = construct_dist_matrix(directed_G, pred, directed=directed) - assert_array_almost_equal(SP1, SP2) - - for method in methods: - for directed in (True, False): - check(method, directed) - - -def test_unweighted_path(): - def check(method, directed): - SP1 = shortest_path(directed_G, - directed=directed, - overwrite=False, - unweighted=True) - SP2 = shortest_path(unweighted_G, - directed=directed, - overwrite=False, - unweighted=False) - assert_array_almost_equal(SP1, SP2) - - for method in methods: - for directed in (True, False): - check(method, directed) - - -def test_negative_cycles(): - # create a small graph with a negative cycle - graph = np.ones([5, 5]) - graph.flat[::6] = 0 - graph[1, 2] = -2 - - def check(method, directed): - assert_raises(NegativeCycleError, shortest_path, graph, method, - directed) - - for directed in (True, False): - for method in ['FW', 'J', 'BF']: - check(method, directed) - - assert_raises(NegativeCycleError, yen, graph, 0, 1, 1, - directed=directed) - - -@pytest.mark.parametrize("method", ['FW', 'J', 'BF']) -def test_negative_weights(method): - SP = shortest_path(directed_negative_weighted_G, method, directed=True) - assert_allclose(SP, directed_negative_weighted_SP, atol=1e-10) - - -def test_masked_input(): - np.ma.masked_equal(directed_G, 0) - - def check(method): - SP = shortest_path(directed_G, method=method, directed=True, - overwrite=False) - assert_array_almost_equal(SP, directed_SP) - - for method in methods: - check(method) - - -def test_overwrite(): - G = np.array([[0, 3, 3, 1, 2], - [3, 0, 0, 2, 4], - [3, 0, 0, 0, 0], - [1, 2, 0, 0, 2], - [2, 4, 0, 2, 0]], dtype=float) - foo = G.copy() - shortest_path(foo, overwrite=False) - assert_array_equal(foo, G) - - -@pytest.mark.parametrize('method', methods) -def test_buffer(method): - # Smoke test that sparse matrices with read-only buffers (e.g., those from - # joblib workers) do not cause:: - # - # ValueError: buffer source array is read-only - # - G = scipy.sparse.csr_matrix([[1.]]) - G.data.flags['WRITEABLE'] = False - shortest_path(G, method=method) - - -def test_NaN_warnings(): - with warnings.catch_warnings(record=True) as record: - shortest_path(np.array([[0, 1], [np.nan, 0]])) - for r in record: - assert r.category is not RuntimeWarning - - -def test_sparse_matrices(): - # Test that using lil,csr and csc sparse matrix do not cause error - G_dense = np.array([[0, 3, 0, 0, 0], - [0, 0, -1, 0, 0], - [0, 0, 0, 2, 0], - [0, 0, 0, 0, 4], - [0, 0, 0, 0, 0]], dtype=float) - SP = shortest_path(G_dense) - G_csr = scipy.sparse.csr_matrix(G_dense) - G_csc = scipy.sparse.csc_matrix(G_dense) - G_lil = scipy.sparse.lil_matrix(G_dense) - assert_array_almost_equal(SP, shortest_path(G_csr)) - assert_array_almost_equal(SP, shortest_path(G_csc)) - assert_array_almost_equal(SP, shortest_path(G_lil)) - - -def test_yen_directed(): - distances, predecessors = yen( - directed_G, - source=0, - sink=3, - K=2, - return_predecessors=True - ) - assert_allclose(distances, [5., 9.]) - assert_allclose(predecessors, directed_2SP_0_to_3) - - -def test_yen_undirected(): - distances = yen( - undirected_G, - source=0, - sink=3, - K=4, - ) - assert_allclose(distances, [1., 4., 5., 8.]) - -def test_yen_unweighted(): - # Ask for more paths than there are, verify only the available paths are returned - distances, predecessors = yen( - directed_G, - source=0, - sink=3, - K=4, - unweighted=True, - return_predecessors=True, - ) - assert_allclose(distances, [2., 3.]) - assert_allclose(predecessors, directed_2SP_0_to_3) - -def test_yen_no_paths(): - distances = yen( - directed_G, - source=2, - sink=3, - K=1, - ) - assert distances.size == 0 - -def test_yen_negative_weights(): - distances = yen( - directed_negative_weighted_G, - source=2, - sink=0, - K=1, - ) - assert_allclose(distances, [-2.]) - - -@pytest.mark.parametrize("min_only", (True, False)) -@pytest.mark.parametrize("directed", (True, False)) -@pytest.mark.parametrize("return_predecessors", (True, False)) -@pytest.mark.parametrize("index_dtype", (np.int32, np.int64)) -@pytest.mark.parametrize("indices", (None, [1])) -def test_20904(min_only, directed, return_predecessors, index_dtype, indices): - """Test two failures from gh-20904: int32 and indices-as-None.""" - adj_mat = scipy.sparse.eye(4, format="csr") - adj_mat = scipy.sparse.csr_array( - ( - adj_mat.data, - adj_mat.indices.astype(index_dtype), - adj_mat.indptr.astype(index_dtype), - ), - ) - dijkstra( - adj_mat, - directed, - indices=indices, - min_only=min_only, - return_predecessors=return_predecessors, - ) diff --git a/tests/csgraph/test_spanning_tree.py b/tests/csgraph/test_spanning_tree.py deleted file mode 100644 index 90ef6d1b1b..0000000000 --- a/tests/csgraph/test_spanning_tree.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Test the minimum spanning tree function""" -import numpy as np -from numpy.testing import assert_ -import numpy.testing as npt -from scipy.sparse import csr_matrix -from scipy.sparse.csgraph import minimum_spanning_tree - - -def test_minimum_spanning_tree(): - - # Create a graph with two connected components. - graph = [[0,1,0,0,0], - [1,0,0,0,0], - [0,0,0,8,5], - [0,0,8,0,1], - [0,0,5,1,0]] - graph = np.asarray(graph) - - # Create the expected spanning tree. - expected = [[0,1,0,0,0], - [0,0,0,0,0], - [0,0,0,0,5], - [0,0,0,0,1], - [0,0,0,0,0]] - expected = np.asarray(expected) - - # Ensure minimum spanning tree code gives this expected output. - csgraph = csr_matrix(graph) - mintree = minimum_spanning_tree(csgraph) - mintree_array = mintree.toarray() - npt.assert_array_equal(mintree_array, expected, - 'Incorrect spanning tree found.') - - # Ensure that the original graph was not modified. - npt.assert_array_equal(csgraph.toarray(), graph, - 'Original graph was modified.') - - # Now let the algorithm modify the csgraph in place. - mintree = minimum_spanning_tree(csgraph, overwrite=True) - npt.assert_array_equal(mintree.toarray(), expected, - 'Graph was not properly modified to contain MST.') - - np.random.seed(1234) - for N in (5, 10, 15, 20): - - # Create a random graph. - graph = 3 + np.random.random((N, N)) - csgraph = csr_matrix(graph) - - # The spanning tree has at most N - 1 edges. - mintree = minimum_spanning_tree(csgraph) - assert_(mintree.nnz < N) - - # Set the sub diagonal to 1 to create a known spanning tree. - idx = np.arange(N-1) - graph[idx,idx+1] = 1 - csgraph = csr_matrix(graph) - mintree = minimum_spanning_tree(csgraph) - - # We expect to see this pattern in the spanning tree and otherwise - # have this zero. - expected = np.zeros((N, N)) - expected[idx, idx+1] = 1 - - npt.assert_array_equal(mintree.toarray(), expected, - 'Incorrect spanning tree found.') diff --git a/tests/csgraph/test_traversal.py b/tests/csgraph/test_traversal.py deleted file mode 100644 index 414e2d1486..0000000000 --- a/tests/csgraph/test_traversal.py +++ /dev/null @@ -1,81 +0,0 @@ -import numpy as np -import pytest -from numpy.testing import assert_array_almost_equal -from scipy.sparse import csr_array -from scipy.sparse.csgraph import (breadth_first_tree, depth_first_tree, - csgraph_to_dense, csgraph_from_dense) - - -def test_graph_breadth_first(): - csgraph = np.array([[0, 1, 2, 0, 0], - [1, 0, 0, 0, 3], - [2, 0, 0, 7, 0], - [0, 0, 7, 0, 1], - [0, 3, 0, 1, 0]]) - csgraph = csgraph_from_dense(csgraph, null_value=0) - - bfirst = np.array([[0, 1, 2, 0, 0], - [0, 0, 0, 0, 3], - [0, 0, 0, 7, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]]) - - for directed in [True, False]: - bfirst_test = breadth_first_tree(csgraph, 0, directed) - assert_array_almost_equal(csgraph_to_dense(bfirst_test), - bfirst) - - -def test_graph_depth_first(): - csgraph = np.array([[0, 1, 2, 0, 0], - [1, 0, 0, 0, 3], - [2, 0, 0, 7, 0], - [0, 0, 7, 0, 1], - [0, 3, 0, 1, 0]]) - csgraph = csgraph_from_dense(csgraph, null_value=0) - - dfirst = np.array([[0, 1, 0, 0, 0], - [0, 0, 0, 0, 3], - [0, 0, 0, 0, 0], - [0, 0, 7, 0, 0], - [0, 0, 0, 1, 0]]) - - for directed in [True, False]: - dfirst_test = depth_first_tree(csgraph, 0, directed) - assert_array_almost_equal(csgraph_to_dense(dfirst_test), - dfirst) - - -def test_graph_breadth_first_trivial_graph(): - csgraph = np.array([[0]]) - csgraph = csgraph_from_dense(csgraph, null_value=0) - - bfirst = np.array([[0]]) - - for directed in [True, False]: - bfirst_test = breadth_first_tree(csgraph, 0, directed) - assert_array_almost_equal(csgraph_to_dense(bfirst_test), - bfirst) - - -def test_graph_depth_first_trivial_graph(): - csgraph = np.array([[0]]) - csgraph = csgraph_from_dense(csgraph, null_value=0) - - bfirst = np.array([[0]]) - - for directed in [True, False]: - bfirst_test = depth_first_tree(csgraph, 0, directed) - assert_array_almost_equal(csgraph_to_dense(bfirst_test), - bfirst) - - -@pytest.mark.parametrize('directed', [True, False]) -@pytest.mark.parametrize('tree_func', [breadth_first_tree, depth_first_tree]) -def test_int64_indices(tree_func, directed): - # See https://github.com/scipy/scipy/issues/18716 - g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2)) - assert g.indices.dtype == np.int64 - tree = tree_func(g, 0, directed=directed) - assert_array_almost_equal(csgraph_to_dense(tree), [[0, 1], [0, 0]]) - From 0f87f578a643e8319212afc39cec6e4bd55abb96 Mon Sep 17 00:00:00 2001 From: Allen Goodman Date: Fri, 4 Oct 2024 10:00:01 -0400 Subject: [PATCH 4/8] graph operators --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 1cfc4bc3f552fa76f071d027e38baf61a4ccf72f Mon Sep 17 00:00:00 2001 From: Allen Goodman Date: Fri, 4 Oct 2024 10:01:06 -0400 Subject: [PATCH 5/8] graph operators --- tests/beignet/test__graph_matrix_to_masked_tensor.py | 2 +- tests/beignet/test__tensor_to_graph_matrix.py | 2 +- tests/beignet/test__tensor_to_masked_graph_matrix.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/beignet/test__graph_matrix_to_masked_tensor.py b/tests/beignet/test__graph_matrix_to_masked_tensor.py index c99149ac4b..a43a0e35da 100644 --- a/tests/beignet/test__graph_matrix_to_masked_tensor.py +++ b/tests/beignet/test__graph_matrix_to_masked_tensor.py @@ -1,2 +1,2 @@ def test_graph_matrix_to_masked_tensor(): - assert False \ No newline at end of file + assert False diff --git a/tests/beignet/test__tensor_to_graph_matrix.py b/tests/beignet/test__tensor_to_graph_matrix.py index 8d03fff881..9d9d2be4ec 100644 --- a/tests/beignet/test__tensor_to_graph_matrix.py +++ b/tests/beignet/test__tensor_to_graph_matrix.py @@ -1,2 +1,2 @@ def test_tensor_to_graph_matrix(): - assert False \ No newline at end of file + assert False diff --git a/tests/beignet/test__tensor_to_masked_graph_matrix.py b/tests/beignet/test__tensor_to_masked_graph_matrix.py index 2fadb3b68f..fa26053a54 100644 --- a/tests/beignet/test__tensor_to_masked_graph_matrix.py +++ b/tests/beignet/test__tensor_to_masked_graph_matrix.py @@ -1,2 +1,2 @@ def test_tensor_to_masked_graph_matrix(): - assert False \ No newline at end of file + assert False From 2446458710cb7c22cfe3ea0f9dc78b0790c06ec7 Mon Sep 17 00:00:00 2001 From: Allen Goodman Date: Fri, 4 Oct 2024 10:01:39 -0400 Subject: [PATCH 6/8] graph operators --- tests/beignet/test__predecessor_matrix_to_distance_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/beignet/test__predecessor_matrix_to_distance_matrix.py b/tests/beignet/test__predecessor_matrix_to_distance_matrix.py index 4a558ddc51..4fc0297f9d 100644 --- a/tests/beignet/test__predecessor_matrix_to_distance_matrix.py +++ b/tests/beignet/test__predecessor_matrix_to_distance_matrix.py @@ -1,2 +1,2 @@ def test_predecessor_matrix_to_distance_matrix(): - assert False \ No newline at end of file + assert False From af2648df06687122dc66772497c66b949688b67e Mon Sep 17 00:00:00 2001 From: Allen Goodman Date: Fri, 4 Oct 2024 10:02:31 -0400 Subject: [PATCH 7/8] graph operators --- src/beignet/_graph_matrix_to_tensor.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/beignet/_graph_matrix_to_tensor.py b/src/beignet/_graph_matrix_to_tensor.py index 2cdef20ed8..7d86e8cf1f 100644 --- a/src/beignet/_graph_matrix_to_tensor.py +++ b/src/beignet/_graph_matrix_to_tensor.py @@ -4,19 +4,6 @@ from scipy.sparse import csr_matrix def _populate_graph(data, indices, indptr, graph, null_value): - """ - Populate the dense graph matrix from CSR sparse matrix attributes. - - Parameters: - - data: 1D numpy array of the non-zero values in the CSR matrix. - - indices: 1D numpy array of column indices corresponding to data. - - indptr: 1D numpy array of index pointers for the CSR matrix. - - graph: 2D numpy array (N x N) initialized with infinities. - - null_value: The value to assign to null entries in the graph. - - The function fills the graph with the minimum edge weights from the CSR matrix, - and assigns null_value to positions where there are no edges. - """ N = graph.shape[0] null_flag = numpy.ones((N, N), dtype=bool, order='C') @@ -35,9 +22,10 @@ def _populate_graph(data, indices, indptr, graph, null_value): # Assign null_value to positions with no edges graph[null_flag] = null_value -def graph_matrix_to_tensor(input: csr_matrix, null_value: float = 0) -> numpy.ndarray: - # Allow only csr, lil and csc matrices: other formats when converted to csr - # combine duplicated edges: we don't want this to happen in the background. +def graph_matrix_to_tensor( + input: csr_matrix, + null_value: float = 0, +) -> numpy.ndarray: if not scipy.sparse.issparse(input): raise ValueError From 795197547482776d6a90f56350c2489e3dd79056 Mon Sep 17 00:00:00 2001 From: Allen Goodman Date: Mon, 3 Feb 2025 14:47:07 -0500 Subject: [PATCH 8/8] floyd_warshall --- src/beignet/_floyd_warshall.py | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/beignet/_floyd_warshall.py diff --git a/src/beignet/_floyd_warshall.py b/src/beignet/_floyd_warshall.py new file mode 100644 index 0000000000..a68e134628 --- /dev/null +++ b/src/beignet/_floyd_warshall.py @@ -0,0 +1,61 @@ +import torch +from torch import Tensor + + +def floyd_warshall( + input: Tensor, + directed: bool = True, + unweighted: bool = False, +): + r""" + ... + + Parameters + ---------- + input : Tensor, shape=(..., N, N) + ... + + directed : bool + If `False`, symmetrizes `input`. Default, `True`. + + unweighted : bool + If `True`, distance of non-zero connections is 1. Default, `False`. + + Returns + ------- + output : Tensor, shape=(..., N, N) + ... + """ + output = input.clone() + + if not directed: + output = 0.5 * (output + output.transpose(-1, -2)) + + if unweighted: + output = torch.where( + output != 0, + torch.ones_like(output), + torch.zeros_like(output), + ) + + n = output.shape[-1] + + eye = torch.eye(n, device=output.device, dtype=output.dtype) + + eye = torch.expand_copy(eye, output.shape) + + output[((output == 0) & (~eye.bool()))] = torch.inf + + output = torch.where( + eye.to(dtype=torch.bool), + torch.zeros_like(output), + output, + ) + + for k in range(n): + a = torch.unsqueeze(output[..., :, k], dim=-1) + b = torch.unsqueeze(output[..., k, :], dim=-2) + + output = torch.minimum(output, a + b) + + return output