Skip to content

Add triangles #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions graphblas_algorithms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import _version
from .cluster import triangles # noqa
from .link_analysis import pagerank # noqa

__version__ = _version.get_versions()["version"]
60 changes: 60 additions & 0 deletions graphblas_algorithms/cluster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from collections import OrderedDict

import graphblas as gb
from graphblas import Matrix, Vector, binary, select
from graphblas.semiring import any_pair, plus_pair
from networkx.utils import not_implemented_for


def single_triangle_core(G, index):
M = Matrix(bool, G.nrows, G.ncols)
M[index, index] = False
C = any_pair(G.T @ M.T).new(name="C") # select.coleq(G, index)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check it out, @jim22k, a use case for coleq select op!

Hmm, the comment is wrong. Should be select.coleq(G.T, index) or select.col(G.T == index).

del C[index, index] # Ignore self-edges
R = C.T.new(name="R")
return plus_pair(G @ R.T).new(mask=C.S).reduce_scalar(allow_empty=False).value // 2


def triangles_core(G, mask=None):
# Ignores self-edges
L = select.tril(G, -1).new(name="L")
U = select.triu(G, 1).new(name="U")
C = plus_pair(L @ L.T).new(mask=L.S)
return (
C.reduce_rowwise().new(mask=mask)
+ C.reduce_columnwise().new(mask=mask)
+ plus_pair(U @ L.T).new(mask=U.S).reduce_rowwise().new(mask=mask)
).new(name="triangles")
Comment on lines +80 to +85
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish I had an intuitive explanation for why this worked (I'm sure one exists if you twist your brain just so). There are simpler methods, but this implementation seems pretty fast.

Here's an example of a simpler method:

    L = gb.select.tril(G, -1).new(name="L")
    B = gb.select.offdiag(G).new(name="offdiag")
    return plus_pair(B @ L.T).new(mask=B.S).reduce_rowwise().new(mask=mask)



def total_triangles_core(G):
# Ignores self-edges
L = select.tril(G, -1).new(name="L")
U = select.triu(G, 1).new(name="U")
return plus_pair(L @ U.T).new(mask=L.S).reduce_scalar(allow_empty=False).value


@not_implemented_for("directed")
def triangles(G, nodes=None):
N = len(G)
if N == 0:
return {}
node_ids = OrderedDict((k, i) for i, k in enumerate(G))
A = gb.io.from_networkx(G, nodelist=node_ids, weight=None, dtype=bool)
if nodes in G:
return single_triangle_core(A, node_ids[nodes])
if nodes is not None:
id_to_key = {node_ids[key]: key for key in nodes}
mask = Vector.from_values(list(id_to_key), True, size=N, dtype=bool, name="mask").S
else:
mask = None
result = triangles_core(A, mask=mask)
if nodes is not None:
if result.nvals != len(id_to_key):
result(mask, binary.first) << 0
indices, values = result.to_values()
return {id_to_key[index]: value for index, value in zip(indices, values)}
elif result.nvals != N:
# Fill with zero
result(mask=~result.S) << 0
return dict(zip(node_ids, result.to_values()[1]))
19 changes: 19 additions & 0 deletions graphblas_algorithms/tests/test_cluster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import inspect

import networkx as nx

from graphblas_algorithms import triangles

nx_triangles = nx.triangles
nx.triangles = triangles
nx.algorithms.triangles = triangles
nx.algorithms.cluster.triangles = triangles


def test_signatures():
nx_sig = inspect.signature(nx_triangles)
sig = inspect.signature(triangles)
assert nx_sig == sig


from networkx.algorithms.tests.test_cluster import * # noqa isort:skip