Skip to content
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

Add a few more BFS-based algorithms #51

Merged
merged 7 commits into from
Apr 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
33 changes: 25 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
# To run: `pre-commit run --all-files`
# To update: `pre-commit autoupdate`
# - &flake8_dependencies below needs updated manually
ci:
# See: https://pre-commit.ci/#configuration
autofix_prs: false
autoupdate_schedule: monthly
skip: [no-commit-to-branch]
fail_fast: true
default_language_version:
python: python3
Expand All @@ -20,12 +25,13 @@ repos:
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.12.1
rev: v0.12.2
hooks:
- id: validate-pyproject
name: Validate pyproject.toml
# I don't yet trust ruff to do what autoflake does
- repo: https://github.com/myint/autoflake
rev: v2.0.1
rev: v2.0.2
hooks:
- id: autoflake
args: [--in-place]
Expand All @@ -44,36 +50,47 @@ repos:
- id: auto-walrus
args: [--line-length, "100"]
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.3.0
hooks:
- id: black
# - id: black-jupyter
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.261
hooks:
- id: ruff
args: [--fix-only, --show-fixes]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: &flake8_dependencies
# These versions need updated manually
- flake8==6.0.0
- flake8-comprehensions==3.10.1
- flake8-bugbear==23.2.13
- flake8-simplify==0.19.3
- flake8-bugbear==23.3.23
- flake8-simplify==0.20.0
- repo: https://github.com/asottile/yesqa
rev: v1.4.0
hooks:
- id: yesqa
additional_dependencies: *flake8_dependencies
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
rev: v2.2.4
hooks:
- id: codespell
types_or: [python, rst, markdown]
additional_dependencies: [tomli]
files: ^(graphblas_algorithms|docs)/
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.253
rev: v0.0.261
hooks:
- id: ruff
# `pyroma` may help keep our package standards up to date if best practices change.
# This is probably a "low value" check though and safe to remove if we want faster pre-commit.
- repo: https://github.com/regebro/pyroma
rev: "4.2"
hooks:
- id: pyroma
args: [-n, "10", .]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ dispatch pattern shown above.
- Community
- inter_community_edges
- intra_community_edges
- Components
- is_connected
- is_weakly_connected
- node_connected_component
- Core
- k_truss
- Cuts
Expand Down Expand Up @@ -147,11 +151,15 @@ dispatch pattern shown above.
- is_k_regular
- is_regular
- Shortest Paths
- all_pairs_bellman_ford_path_length
- all_pairs_shortest_path_length
- floyd_warshall
- floyd_warshall_predecessor_and_distance
- single_source_bellman_ford_path_length
- all_pairs_bellman_ford_path_length
- has_path
- negative_edge_cycle
- single_source_bellman_ford_path_length
- single_source_shortest_path_length
- single_target_shortest_path_length
- Simple Paths
- is_simple_path
- S Metric
Expand All @@ -162,5 +170,8 @@ dispatch pattern shown above.
- is_tournament
- score_sequence
- tournament_matrix
- Traversal
- bfs_layers
- descendants_at_distance
- Triads
- is_triad
2 changes: 2 additions & 0 deletions graphblas_algorithms/algorithms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .centrality import *
from .cluster import *
from .community import *
from .components import *
from .core import *
from .cuts import *
from .dag import *
Expand All @@ -16,4 +17,5 @@
from .smetric import *
from .structuralholes import *
from .tournament import *
from .traversal import *
from .triads import *
2 changes: 1 addition & 1 deletion graphblas_algorithms/algorithms/centrality/eigenvector.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def eigenvector_centrality(G, max_iter=100, tol=1.0e-6, nstart=None, name="eigen
# Power iteration: make up to max_iter iterations
A = G._A
xprev = Vector(float, N, name="x_prev")
for _ in range(max_iter):
for _i in range(max_iter):
xprev << x
x += x @ A
normalize(x, "L2")
Expand Down
2 changes: 1 addition & 1 deletion graphblas_algorithms/algorithms/centrality/katz.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def katz_centrality(

# Power iteration: make up to max_iter iterations
xprev = Vector(float, N, name="x_prev")
for _ in range(max_iter):
for _i in range(max_iter):
xprev, x = x, xprev
# x << alpha * semiring(xprev @ A) + beta
x << semiring(xprev @ A)
Expand Down
2 changes: 2 additions & 0 deletions graphblas_algorithms/algorithms/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .connected import *
from .weakly_connected import *
31 changes: 31 additions & 0 deletions graphblas_algorithms/algorithms/components/connected.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from graphblas import Vector, replace
from graphblas.semiring import any_pair

from graphblas_algorithms.algorithms.exceptions import PointlessConcept


def is_connected(G):
if len(G) == 0:
raise PointlessConcept("Connectivity is undefined for the null graph.")
return _plain_bfs(G, next(iter(G))).nvals == len(G)


def node_connected_component(G, n):
return _plain_bfs(G, n)


def _plain_bfs(G, source):
index = G._key_to_id[source]
A = G.get_property("offdiag")
n = A.nrows
v = Vector(bool, n, name="bfs_plain")
q = Vector(bool, n, name="q")
v[index] = True
q[index] = True
any_pair_bool = any_pair[bool]
for _i in range(1, n):
q(~v.S, replace) << any_pair_bool(q @ A)
if q.nvals == 0:
break
v(q.S) << True
return v
77 changes: 77 additions & 0 deletions graphblas_algorithms/algorithms/components/weakly_connected.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from graphblas import Vector, binary, replace
from graphblas.semiring import any_pair

from graphblas_algorithms.algorithms.exceptions import PointlessConcept


def is_weakly_connected(G):
if len(G) == 0:
raise PointlessConcept("Connectivity is undefined for the null graph.")
return _plain_bfs(G, next(iter(G))).nvals == len(G)


# TODO: benchmark this and the version commented out below
def _plain_bfs(G, source):
# Bi-directional BFS w/o symmetrizing the adjacency matrix
index = G._key_to_id[source]
A = G.get_property("offdiag")
# XXX: should we use `AT` if available?
n = A.nrows
v = Vector(bool, n, name="bfs_plain")
q_out = Vector(bool, n, name="q_out")
q_in = Vector(bool, n, name="q_in")
v[index] = True
q_in[index] = True
any_pair_bool = any_pair[bool]
is_out_empty = True
is_in_empty = False
for _i in range(1, n):
# Traverse out-edges from the most recent `q_in` and `q_out`
if is_out_empty:
q_out(~v.S) << any_pair_bool(q_in @ A)
else:
q_out << binary.any(q_out | q_in)
q_out(~v.S, replace) << any_pair_bool(q_out @ A)
is_out_empty = q_out.nvals == 0
if not is_out_empty:
v(q_out.S) << True
elif is_in_empty:
break
# Traverse in-edges from the most recent `q_in` and `q_out`
if is_in_empty:
q_in(~v.S) << any_pair_bool(A @ q_out)
else:
q_in << binary.any(q_out | q_in)
q_in(~v.S, replace) << any_pair_bool(A @ q_in)
is_in_empty = q_in.nvals == 0
if not is_in_empty:
v(q_in.S) << True
elif is_out_empty:
break
return v


"""
def _plain_bfs(G, source):
# Bi-directional BFS w/o symmetrizing the adjacency matrix
index = G._key_to_id[source]
A = G.get_property("offdiag")
n = A.nrows
v = Vector(bool, n, name="bfs_plain")
q = Vector(bool, n, name="q")
q2 = Vector(bool, n, name="q_2")
v[index] = True
q[index] = True
any_pair_bool = any_pair[bool]
for _i in range(1, n):
q2(~v.S, replace) << any_pair_bool(q @ A)
v(q2.S) << True
q(~v.S, replace) << any_pair_bool(A @ q)
if q.nvals == 0:
if q2.nvals == 0:
break
q, q2 = q2, q
elif q2.nvals != 0:
q << binary.any(q | q2)
return v
"""
18 changes: 11 additions & 7 deletions graphblas_algorithms/algorithms/dag.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from graphblas import Vector, replace
from graphblas.semiring import lor_pair
from graphblas.semiring import any_pair

__all__ = ["descendants", "ancestors"]

Expand All @@ -10,10 +10,12 @@ def descendants(G, source):
raise KeyError(f"The node {source} is not in the graph")
index = G._key_to_id[source]
A = G.get_property("offdiag")
q = Vector.from_coo(index, True, size=A.nrows, name="q")
q = Vector(bool, size=A.nrows, name="q")
q[index] = True
rv = q.dup(name="descendants")
for _ in range(A.nrows):
q(~rv.S, replace) << lor_pair(q @ A)
any_pair_bool = any_pair[bool]
for _i in range(A.nrows):
q(~rv.S, replace) << any_pair_bool(q @ A)
if q.nvals == 0:
break
rv(q.S) << True
Expand All @@ -26,10 +28,12 @@ def ancestors(G, source):
raise KeyError(f"The node {source} is not in the graph")
index = G._key_to_id[source]
A = G.get_property("offdiag")
q = Vector.from_coo(index, True, size=A.nrows, name="q")
q = Vector(bool, size=A.nrows, name="q")
q[index] = True
rv = q.dup(name="descendants")
for _ in range(A.nrows):
q(~rv.S, replace) << lor_pair(A @ q)
any_pair_bool = any_pair[bool]
for _i in range(A.nrows):
q(~rv.S, replace) << any_pair_bool(A @ q)
if q.nvals == 0:
break
rv(q.S) << True
Expand Down
4 changes: 2 additions & 2 deletions graphblas_algorithms/algorithms/dominating.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from graphblas.semiring import lor_pair
from graphblas.semiring import any_pair

__all__ = ["is_dominating_set"]


def is_dominating_set(G, nbunch):
nbrs = lor_pair(nbunch @ G._A).new(mask=~nbunch.S) # A or A.T?
nbrs = any_pair[bool](nbunch @ G._A).new(mask=~nbunch.S) # A or A.T?
return nbrs.size - nbunch.nvals - nbrs.nvals == 0
4 changes: 4 additions & 0 deletions graphblas_algorithms/algorithms/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ class PointlessConcept(GraphBlasAlgorithmException):
pass


class NoPath(GraphBlasAlgorithmException):
pass


class Unbounded(GraphBlasAlgorithmException):
pass
4 changes: 2 additions & 2 deletions graphblas_algorithms/algorithms/link_analysis/hits_alg.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def hits(G, max_iter=100, tol=1.0e-8, nstart=None, normalized=True, *, with_auth
a, h = h, a
ATA = (A.T @ A).new(name="ATA") # Authority matrix
aprev = Vector(float, N, name="a_prev")
for _ in range(max_iter):
for _i in range(max_iter):
aprev, a = a, aprev
a << ATA @ aprev
normalize(a, "Linf")
Expand All @@ -41,7 +41,7 @@ def hits(G, max_iter=100, tol=1.0e-8, nstart=None, normalized=True, *, with_auth
raise ConvergenceFailure(max_iter)
else:
hprev = Vector(float, N, name="h_prev")
for _ in range(max_iter):
for _i in range(max_iter):
hprev, h = h, hprev
a << hprev @ A
h << A @ a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def pagerank(
# Power iteration: make up to max_iter iterations
xprev = Vector(float, N, name="x_prev")
w = Vector(float, N, name="w")
for _ in range(max_iter):
for _i in range(max_iter):
xprev, x = x, xprev

# x << alpha * ((xprev * S) @ A + "dangling_weights") + (1 - alpha) * p
Expand Down
1 change: 1 addition & 0 deletions graphblas_algorithms/algorithms/shortest_paths/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .dense import *
from .generic import *
from .unweighted import *
from .weighted import *
23 changes: 13 additions & 10 deletions graphblas_algorithms/algorithms/shortest_paths/generic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from graphblas import Vector, replace
from graphblas.semiring import lor_pair
from graphblas.semiring import any_pair

__all__ = ["has_path"]

Expand All @@ -11,23 +11,26 @@ def has_path(G, source, target):
if src == dst:
return True
A = G.get_property("offdiag")
q_src = Vector.from_coo(src, True, size=A.nrows, name="q_src")
q_src = Vector(bool, size=A.nrows, name="q_src")
q_src[src] = True
seen_src = q_src.dup(name="seen_src")
q_dst = Vector.from_coo(dst, True, size=A.nrows, name="q_dst")
seen_dst = q_dst.dup(name="seen_dst")
for _ in range(A.nrows // 2):
q_src(~seen_src.S, replace) << lor_pair(q_src @ A)
q_dst = Vector(bool, size=A.nrows, name="q_dst")
q_dst[dst] = True
seen_dst = q_dst.dup(name="seen_dst", clear=True)
any_pair_bool = any_pair[bool]
for _i in range(A.nrows // 2):
q_src(~seen_src.S, replace) << any_pair_bool(q_src @ A)
if q_src.nvals == 0:
return False
if lor_pair(q_src @ q_dst):
if any_pair_bool(q_src @ q_dst):
return True

q_dst(~seen_dst.S, replace) << lor_pair(A @ q_dst)
seen_dst(q_dst.S) << True
q_dst(~seen_dst.S, replace) << any_pair_bool(A @ q_dst)
if q_dst.nvals == 0:
return False
if lor_pair(q_src @ q_dst):
if any_pair_bool(q_src @ q_dst):
return True

seen_src(q_src.S) << True
seen_dst(q_dst.S) << True
return False
Loading