diff --git a/graphblas_algorithms/__init__.py b/graphblas_algorithms/__init__.py index fce1f5d..7009463 100644 --- a/graphblas_algorithms/__init__.py +++ b/graphblas_algorithms/__init__.py @@ -1,5 +1,6 @@ from . import _version from .cluster import average_clustering, clustering, transitivity, triangles # noqa from .link_analysis import pagerank # noqa +from .reciprocity import overall_reciprocity, reciprocity # noqa __version__ = _version.get_versions()["version"] diff --git a/graphblas_algorithms/cluster.py b/graphblas_algorithms/cluster.py index 24f08f3..0c02926 100644 --- a/graphblas_algorithms/cluster.py +++ b/graphblas_algorithms/cluster.py @@ -51,11 +51,10 @@ def get_degrees(G, mask=None, *, L=None, U=None, has_self_edges=True): if L is None or U is None: L, U = get_properties(G, "L U", L=L, U=U) degrees = ( - L.reduce_rowwise(gb.agg.count).new(mask=mask) - + U.reduce_rowwise(gb.agg.count).new(mask=mask) + L.reduce_rowwise("count").new(mask=mask) + U.reduce_rowwise("count").new(mask=mask) ).new(name="degrees") else: - degrees = G.reduce_rowwise(gb.agg.count).new(mask=mask, name="degrees") + degrees = G.reduce_rowwise("count").new(mask=mask, name="degrees") return degrees @@ -120,7 +119,7 @@ def transitivity_directed_core(G, *, has_self_edges=True): numerator = plus_pair(A @ A.T).new(mask=A.S).reduce_scalar(allow_empty=False).value if numerator == 0: return 0 - deg = A.reduce_rowwise(gb.agg.count) + deg = A.reduce_rowwise("count") denom = (deg * (deg - 1)).reduce().value return numerator / denom @@ -160,9 +159,9 @@ def clustering_directed_core(G, mask=None, *, has_self_edges=True): + plus_pair(AT @ AT.T).new(mask=A.S).reduce_columnwise().new(mask=mask) ) recip_degrees = binary.pair(A & AT).reduce_rowwise().new(mask=mask) - total_degrees = ( - A.reduce_rowwise(gb.agg.count).new(mask=mask) + A.reduce_columnwise(gb.agg.count) - ).new(mask=mask) + total_degrees = A.reduce_rowwise("count").new(mask=mask) + A.reduce_columnwise("count").new( + mask=mask + ) return (tri / (total_degrees * (total_degrees - 1) - 2 * recip_degrees)).new(name="clustering") diff --git a/graphblas_algorithms/conftest.py b/graphblas_algorithms/conftest.py index c598c08..8dc72a6 100644 --- a/graphblas_algorithms/conftest.py +++ b/graphblas_algorithms/conftest.py @@ -1 +1,38 @@ +import inspect +import sys +import types + +import networkx as nx +import pytest from networkx.conftest import * # noqa + +import graphblas_algorithms as ga + + +class Orig: + pass + + +@pytest.fixture(scope="session", autouse=True) +def orig(): + """Monkey-patch networkx with functions from graphblas-algorithms""" + # This doesn't replace functions that have been renamed such as via `import xxx as _xxx` + orig = Orig() + replacements = { + key: (getattr(nx, key), val) + for key, val in vars(ga).items() + if not key.startswith("_") and hasattr(nx, key) and not isinstance(val, types.ModuleType) + } + replacements["pagerank_scipy"] = (nx.pagerank_scipy, ga.pagerank) + replacements["pagerank_numpy"] = (nx.pagerank_numpy, ga.pagerank) + for key, (orig_val, new_val) in replacements.items(): + setattr(orig, key, orig_val) + if key not in {"pagerank_numpy"}: + assert inspect.signature(orig_val) == inspect.signature(new_val), key + for name, module in sys.modules.items(): + if not name.startswith("networkx.") and name != "networkx": + continue + for key, (orig_val, new_val) in replacements.items(): + if getattr(module, key, None) is orig_val: + setattr(module, key, new_val) + yield orig diff --git a/graphblas_algorithms/link_analysis.py b/graphblas_algorithms/link_analysis.py index bf8389d..aa2093a 100644 --- a/graphblas_algorithms/link_analysis.py +++ b/graphblas_algorithms/link_analysis.py @@ -20,10 +20,10 @@ def pagerank_core( ): N = A.nrows if A.nvals == 0: - return Vector.new(float, N, name=name) + return Vector(float, N, name=name) # Initial vector - x = Vector.new(float, N, name="x") + x = Vector(float, N, name="x") if nstart is None: x[:] = 1.0 / N else: @@ -61,7 +61,7 @@ def pagerank_core( is_dangling = S.nvals < N if is_dangling: - dangling_mask = Vector.new(float, N, name="dangling_mask") + dangling_mask = Vector(float, N, name="dangling_mask") dangling_mask(mask=~S.S) << 1.0 # Fold alpha constant into dangling_weights (or dangling_mask) if dangling is not None: @@ -78,8 +78,8 @@ def pagerank_core( p *= 1 - alpha # Power iteration: make up to max_iter iterations - xprev = Vector.new(float, N, name="x_prev") - w = Vector.new(float, N, name="w") + xprev = Vector(float, N, name="x_prev") + w = Vector(float, N, name="w") for _ in range(max_iter): xprev, x = x, xprev diff --git a/graphblas_algorithms/reciprocity.py b/graphblas_algorithms/reciprocity.py new file mode 100644 index 0000000..81eac51 --- /dev/null +++ b/graphblas_algorithms/reciprocity.py @@ -0,0 +1,51 @@ +from graphblas import binary +from networkx import NetworkXError +from networkx.utils import not_implemented_for + +from ._utils import graph_to_adjacency, list_to_mask, vector_to_dict + + +def reciprocity_core(G, mask=None): + # TODO: used cached properties + overlap = binary.pair(G & G.T).reduce_rowwise().new(mask=mask) + total_degrees = G.reduce_rowwise("count").new(mask=mask) + G.reduce_columnwise("count").new( + mask=mask + ) + return binary.truediv(2 * overlap | total_degrees, left_default=0, right_default=0).new( + name="reciprocity" + ) + + +@not_implemented_for("undirected", "multigraph") +def reciprocity(G, nodes=None): + if nodes is None: + return overall_reciprocity(G) + A, key_to_id = graph_to_adjacency(G, dtype=bool) + if nodes in G: + mask, id_to_key = list_to_mask([nodes], key_to_id) + result = reciprocity_core(A, mask=mask) + rv = result[key_to_id[nodes]].value + if rv is None: + raise NetworkXError("Not defined for isolated nodes.") + else: + return rv + else: + mask, id_to_key = list_to_mask(nodes, key_to_id) + result = reciprocity_core(A, mask=mask) + return vector_to_dict(result, key_to_id, id_to_key, mask=mask) + + +def overall_reciprocity_core(G, *, has_self_edges=True): + n_all_edge = G.nvals + if n_all_edge == 0: + raise NetworkXError("Not defined for empty graphs") + n_overlap_edges = binary.pair(G & G.T).reduce_scalar(allow_empty=False).value + if has_self_edges: + n_overlap_edges -= G.diag().nvals + return n_overlap_edges / n_all_edge + + +@not_implemented_for("undirected", "multigraph") +def overall_reciprocity(G): + A, key_to_id = graph_to_adjacency(G, dtype=bool) + return overall_reciprocity_core(A) diff --git a/graphblas_algorithms/tests/test_cluster.py b/graphblas_algorithms/tests/test_cluster.py index a3c8401..35336f4 100644 --- a/graphblas_algorithms/tests/test_cluster.py +++ b/graphblas_algorithms/tests/test_cluster.py @@ -1,42 +1,8 @@ -import inspect - import graphblas as gb import networkx as nx import graphblas_algorithms as ga -from graphblas_algorithms import average_clustering, clustering, transitivity, triangles - -nx_triangles = nx.triangles -nx.triangles = triangles -nx.algorithms.triangles = triangles -nx.algorithms.cluster.triangles = triangles - -nx_transitivity = nx.transitivity -nx.transitivity = transitivity -nx.algorithms.transitivity = transitivity -nx.algorithms.cluster.transitivity = transitivity - -nx_clustering = nx.clustering -nx.clustering = clustering -nx.algorithms.clustering = clustering -nx.algorithms.cluster.clustering = clustering - -nx_average_clustering = nx.average_clustering -nx.average_clustering = average_clustering -nx.algorithms.average_clustering = average_clustering -nx.algorithms.cluster.average_clustering = average_clustering - - -def test_signatures(): - nx_sig = inspect.signature(nx_triangles) - sig = inspect.signature(triangles) - assert nx_sig == sig - nx_sig = inspect.signature(nx_transitivity) - sig = inspect.signature(transitivity) - assert nx_sig == sig - nx_sig = inspect.signature(nx_clustering) - sig = inspect.signature(clustering) - assert nx_sig == sig +from graphblas_algorithms import average_clustering, clustering, transitivity, triangles # noqa def test_triangles_full(): @@ -89,32 +55,32 @@ def test_triangles_full(): assert ga.cluster.average_clustering_core(G2, mask=mask.S) == 1 -def test_directed(): +def test_directed(orig): # XXX" is transitivity supposed to work on directed graphs like this? G = nx.complete_graph(5, create_using=nx.DiGraph()) G.remove_edge(1, 2) G.remove_edge(2, 3) G.add_node(5) - expected = nx_transitivity(G) + expected = orig.transitivity(G) result = transitivity(G) assert expected == result # clustering - expected = nx_clustering(G) + expected = orig.clustering(G) result = clustering(G) assert result == expected - expected = nx_clustering(G, [0, 1, 2]) + expected = orig.clustering(G, [0, 1, 2]) result = clustering(G, [0, 1, 2]) assert result == expected for i in range(6): - assert nx_clustering(G, i) == clustering(G, i) + assert orig.clustering(G, i) == clustering(G, i) # average_clustering - expected = nx_average_clustering(G) + expected = orig.average_clustering(G) result = average_clustering(G) assert result == expected - expected = nx_average_clustering(G, [0, 1, 2]) + expected = orig.average_clustering(G, [0, 1, 2]) result = average_clustering(G, [0, 1, 2]) assert result == expected - expected = nx_average_clustering(G, count_zeros=False) + expected = orig.average_clustering(G, count_zeros=False) result = average_clustering(G, count_zeros=False) assert result == expected diff --git a/graphblas_algorithms/tests/test_pagerank.py b/graphblas_algorithms/tests/test_pagerank.py index cdbda24..183b91f 100644 --- a/graphblas_algorithms/tests/test_pagerank.py +++ b/graphblas_algorithms/tests/test_pagerank.py @@ -1,21 +1,3 @@ -import inspect - -import networkx as nx - -from graphblas_algorithms import pagerank - -nx_pagerank = nx.pagerank -nx_pagerank_scipy = nx.pagerank_scipy - -nx.pagerank = pagerank -nx.pagerank_scipy = pagerank -nx.algorithms.link_analysis.pagerank_alg.pagerank_scipy = pagerank - - -def test_signatures(): - nx_sig = inspect.signature(nx_pagerank) - sig = inspect.signature(pagerank) - assert nx_sig == sig - +from graphblas_algorithms import pagerank # noqa from networkx.algorithms.link_analysis.tests.test_pagerank import * # noqa isort:skip diff --git a/graphblas_algorithms/tests/test_reciprocity.py b/graphblas_algorithms/tests/test_reciprocity.py new file mode 100644 index 0000000..93eee39 --- /dev/null +++ b/graphblas_algorithms/tests/test_reciprocity.py @@ -0,0 +1,3 @@ +from graphblas_algorithms import overall_reciprocity, reciprocity # noqa + +from networkx.algorithms.tests.test_reciprocity import * # noqa isort:skip