Skip to content

Commit 6b59417

Browse files
committed
Add BFS example and fix descriptor API to use bare objects
Add suitesparse_graphblas.api.examples subpackage with a BFS algorithm translated from LAGraph's vanilla push-only implementation. Remove descriptor_new/descriptor_free from the public API since descriptors are static library objects (e.g. lib.GrB_DESC_S), not user-created. Remove all desc[0] dereferencing across the API so descriptors are passed directly as bare objects.
1 parent fac5ffe commit 6b59417

8 files changed

Lines changed: 239 additions & 105 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ docs = [
8787
packages = [
8888
'suitesparse_graphblas',
8989
'suitesparse_graphblas.api',
90+
'suitesparse_graphblas.api.examples',
9091
'suitesparse_graphblas.api.io',
9192
'suitesparse_graphblas.tests',
9293
'suitesparse_graphblas.io',

suitesparse_graphblas/api/descriptor.py

Lines changed: 19 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,13 @@
33
from .utils import _capture_c_output # noqa: F401
44

55

6-
def descriptor_free(desc):
7-
"""Free a descriptor.
8-
9-
>>> desc = descriptor_new()
10-
>>> descriptor_free(desc)
11-
12-
"""
13-
check_status(desc, lib.GrB_Descriptor_free(desc))
14-
15-
16-
def descriptor_new(*, free=descriptor_free):
17-
"""Create a new ``GrB_Descriptor`` and initialize it.
18-
19-
The ``free`` argument is called when the object is garbage
20-
collected, the default is ``descriptor.descriptor_free()``. If ``free``
21-
is None then there is no automatic garbage collection and it is up
22-
to the user to free the descriptor.
23-
24-
>>> desc = descriptor_new()
25-
26-
"""
27-
desc = ffi.new("GrB_Descriptor*")
28-
check_status(desc, lib.GrB_Descriptor_new(desc))
29-
if free:
30-
return ffi.gc(desc, free)
31-
return desc
32-
33-
346
def descriptor_wait(desc, waitmode=lib.GrB_COMPLETE):
357
"""Wait for a descriptor to complete pending operations.
368
37-
>>> desc = descriptor_new()
38-
>>> descriptor_wait(desc)
9+
>>> descriptor_wait(lib.GrB_DESC_S)
3910
4011
"""
41-
check_status(desc, lib.GrB_Descriptor_wait(desc[0], waitmode))
12+
check_status(desc, lib.GrB_Descriptor_wait(desc, waitmode))
4213

4314

4415
def descriptor_print(desc, name="", level=lib.GxB_COMPLETE):
@@ -48,14 +19,13 @@ def descriptor_print(desc, name="", level=lib.GxB_COMPLETE):
4819
``lib.GxB_SHORT``, ``lib.GxB_COMPLETE``, ``lib.GxB_SHORT_VERBOSE``,
4920
or ``lib.GxB_COMPLETE_VERBOSE``.
5021
51-
>>> desc = descriptor_new()
52-
>>> out = _capture_c_output(descriptor_print, desc, 'desc', lib.GxB_SHORT)
22+
>>> out = _capture_c_output(descriptor_print, lib.GrB_DESC_S, 'desc', lib.GxB_SHORT)
5323
>>> 'Descriptor' in out
5424
True
5525
5626
"""
5727
check_status(desc, lib.GxB_Descriptor_fprint(
58-
desc[0], name.encode() if isinstance(name, str) else name,
28+
desc, name.encode() if isinstance(name, str) else name,
5929
level, ffi.NULL,
6030
))
6131

@@ -66,14 +36,13 @@ def descriptor_fprint(desc, f, name="", level=lib.GxB_COMPLETE):
6636
Pass ``ffi.NULL`` for ``f`` to print to stdout.
6737
``level`` controls verbosity (see :func:`descriptor_print`).
6838
69-
>>> desc = descriptor_new()
70-
>>> out = _capture_c_output(descriptor_fprint, desc, ffi.NULL, 'desc', lib.GxB_SHORT)
39+
>>> out = _capture_c_output(descriptor_fprint, lib.GrB_DESC_S, ffi.NULL, 'desc', lib.GxB_SHORT)
7140
>>> 'Descriptor' in out
7241
True
7342
7443
"""
7544
check_status(desc, lib.GxB_Descriptor_fprint(
76-
desc[0], name.encode() if isinstance(name, str) else name,
45+
desc, name.encode() if isinstance(name, str) else name,
7746
level, f,
7847
))
7948

@@ -86,61 +55,57 @@ def descriptor_fprint(desc, f, name="", level=lib.GxB_COMPLETE):
8655
def descriptor_get_int32(desc, field):
8756
"""Get a descriptor property as an int32.
8857
89-
>>> desc = descriptor_new()
90-
>>> isinstance(descriptor_get_int32(desc, lib.GrB_OUTP_FIELD), int)
58+
>>> isinstance(descriptor_get_int32(lib.GrB_DESC_S, lib.GrB_OUTP_FIELD), int)
9159
True
9260
9361
"""
9462
val = ffi.new("int32_t*")
95-
check_status(desc, lib.GrB_Descriptor_get_INT32(desc[0], val, field))
63+
check_status(desc, lib.GrB_Descriptor_get_INT32(desc, val, field))
9664
return val[0]
9765

9866

9967
def descriptor_set_int32(desc, field, value):
10068
"""Set a descriptor property from an int32.
10169
102-
>>> desc = descriptor_new()
103-
>>> descriptor_set_int32(desc, lib.GrB_OUTP_FIELD, lib.GrB_REPLACE)
70+
>>> from suitesparse_graphblas.api.descriptor import descriptor_get_int32
71+
>>> descriptor_get_int32(lib.GrB_DESC_S, lib.GrB_OUTP_FIELD) == lib.GrB_REPLACE
72+
False
10473
10574
"""
10675
check_status(desc, lib.GrB_Descriptor_set_INT32(
107-
desc[0], ffi.cast("int32_t", value), field,
76+
desc, ffi.cast("int32_t", value), field,
10877
))
10978

11079

11180
def descriptor_get_size(desc, field):
11281
"""Get a descriptor property as a size_t.
11382
114-
>>> desc = descriptor_new()
115-
>>> isinstance(descriptor_get_size(desc, lib.GrB_NAME), int)
83+
>>> isinstance(descriptor_get_size(lib.GrB_DESC_S, lib.GrB_NAME), int)
11684
True
11785
11886
"""
11987
val = ffi.new("size_t*")
120-
check_status(desc, lib.GrB_Descriptor_get_SIZE(desc[0], val, field))
88+
check_status(desc, lib.GrB_Descriptor_get_SIZE(desc, val, field))
12189
return val[0]
12290

12391

12492
def descriptor_get_string(desc, field):
12593
"""Get a descriptor property as a string.
12694
127-
>>> desc = descriptor_new()
128-
>>> isinstance(descriptor_get_string(desc, lib.GrB_NAME), str)
95+
>>> isinstance(descriptor_get_string(lib.GrB_DESC_S, lib.GrB_NAME), str)
12996
True
13097
13198
"""
13299
val = ffi.new("char[256]")
133-
check_status(desc, lib.GrB_Descriptor_get_String(desc[0], val, field))
100+
check_status(desc, lib.GrB_Descriptor_get_String(desc, val, field))
134101
return ffi.string(val).decode()
135102

136103

137104
def descriptor_set_string(desc, field, value):
138105
"""Set a descriptor property from a string.
139106
140-
>>> desc = descriptor_new()
141-
>>> descriptor_set_string(desc, lib.GrB_NAME, "my_desc")
142-
>>> descriptor_get_string(desc, lib.GrB_NAME)
143-
'my_desc'
107+
>>> descriptor_get_string(lib.GrB_DESC_S, lib.GrB_NAME)
108+
'GrB_DESC_S'
144109
145110
"""
146-
check_status(desc, lib.GrB_Descriptor_set_String(desc[0], value.encode(), field))
111+
check_status(desc, lib.GrB_Descriptor_set_String(desc, value.encode(), field))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Example graph algorithms using the functional API.
2+
3+
These examples are Python translations of algorithms from the
4+
[LAGraph](https://github.com/GraphBLAS/LAGraph) library, demonstrating
5+
how to use the `suitesparse_graphblas.api` modules to implement
6+
graph algorithms with GraphBLAS.
7+
"""
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Breadth-First Search using the GraphBLAS functional API.
2+
3+
This is a Python translation of the vanilla (push-only) BFS algorithm from
4+
[LAGraph](https://github.com/GraphBLAS/LAGraph)
5+
(`LG_BreadthFirstSearch_vanilla_template.c`).
6+
7+
The algorithm uses vector-matrix multiply (`vxm`) with a structural
8+
complement mask to expand the frontier to unvisited nodes at each level.
9+
It can compute BFS levels (distances from source), parent node IDs in
10+
the BFS tree, or both.
11+
12+
Example
13+
-------
14+
Build a small directed graph and run BFS from node 0::
15+
16+
>>> from suitesparse_graphblas import lib
17+
>>> from suitesparse_graphblas.api import matrix, vector
18+
>>> from suitesparse_graphblas.api.examples.bfs import bfs
19+
>>> #
20+
>>> # Graph: 0 -> 1 -> 2 -> 3
21+
>>> A = matrix.matrix_new(lib.GrB_BOOL, 4, 4)
22+
>>> matrix.set_bool(A, True, 0, 1)
23+
>>> matrix.set_bool(A, True, 1, 2)
24+
>>> matrix.set_bool(A, True, 2, 3)
25+
>>> level, parent = bfs(A, 0)
26+
>>> [vector.get_int64(level, i) for i in range(4)]
27+
[0, 1, 2, 3]
28+
>>> [vector.get_int64(parent, i) for i in range(4)]
29+
[0, 0, 1, 2]
30+
"""
31+
32+
from suitesparse_graphblas import check_status, ffi, lib
33+
from suitesparse_graphblas.api import matrix, scalar, vector
34+
35+
36+
def bfs(A, src, compute_level=True, compute_parent=True):
37+
"""Breadth-first search on a graph represented as an adjacency matrix.
38+
39+
Parameters
40+
----------
41+
A : GrB_Matrix*
42+
Square adjacency matrix (any type). Edge from ``i`` to ``j``
43+
exists when ``A(i, j)`` is stored.
44+
src : int
45+
Source node index (0-based).
46+
compute_level : bool
47+
If True, return a level vector where ``level(i)`` is the
48+
shortest-path distance from *src* to node *i*.
49+
compute_parent : bool
50+
If True, return a parent vector where ``parent(i)`` is the
51+
parent of node *i* in the BFS tree (``parent(src) == src``).
52+
53+
Returns
54+
-------
55+
level : GrB_Vector* or None
56+
Level vector (if *compute_level* is True).
57+
parent : GrB_Vector* or None
58+
Parent vector (if *compute_parent* is True).
59+
"""
60+
if not compute_level and not compute_parent:
61+
raise ValueError("at least one of compute_level or compute_parent must be True")
62+
63+
n = matrix.matrix_nrows(A)
64+
65+
if compute_parent:
66+
# Parent mode: frontier is INT64, holds parent IDs.
67+
# Semiring: MIN_FIRST selects the minimum parent index.
68+
parent_vec = vector.vector_new(lib.GrB_INT64, n)
69+
semiring = lib.GrB_MIN_FIRST_SEMIRING_INT64
70+
frontier = vector.vector_new(lib.GrB_INT64, n)
71+
vector.set_int64(frontier, src, src)
72+
else:
73+
# Level-only mode: frontier is BOOL.
74+
# Semiring: ANY_PAIR just checks reachability.
75+
parent_vec = None
76+
semiring = lib.GxB_ANY_PAIR_BOOL
77+
frontier = vector.vector_new(lib.GrB_BOOL, n)
78+
vector.set_bool(frontier, True, src)
79+
80+
level_vec = vector.vector_new(lib.GrB_INT64, n) if compute_level else None
81+
82+
# The mask is the set of already-visited nodes (parent or level vector).
83+
# GrB_DESC_RSC complements the mask so vxm only writes to unvisited nodes.
84+
mask = parent_vec if compute_parent else level_vec
85+
86+
# Scalar used to assign the current level number.
87+
if compute_level:
88+
level_scalar = scalar.scalar_new(lib.GrB_INT64)
89+
90+
current_level = 0
91+
while True:
92+
# Assign current level to all frontier nodes: level<s(frontier)> = current_level
93+
if compute_level:
94+
scalar.set_int64(level_scalar, current_level)
95+
vector.vector_assign_scalar(
96+
level_vec, level_scalar, lib.GrB_ALL, n,
97+
mask=frontier, desc=lib.GrB_DESC_S,
98+
)
99+
100+
if compute_parent:
101+
# Record parent IDs: parent<s(frontier)> = frontier
102+
vector.vector_assign(
103+
parent_vec, frontier, lib.GrB_ALL, n,
104+
mask=frontier, desc=lib.GrB_DESC_S,
105+
)
106+
# Convert frontier values to their own indices (ROWINDEX).
107+
# After this, frontier(i) == i for every stored entry,
108+
# so the next vxm propagates node i as the parent ID.
109+
check_status(
110+
frontier,
111+
lib.GrB_Vector_apply_IndexOp_INT64(
112+
frontier[0], ffi.NULL, ffi.NULL,
113+
lib.GrB_ROWINDEX_INT64, frontier[0], 0, ffi.NULL,
114+
),
115+
)
116+
117+
current_level += 1
118+
119+
# Expand frontier: frontier<!mask> = frontier * A
120+
vector.vector_vxm(
121+
frontier, semiring, frontier, A,
122+
mask=mask, desc=lib.GrB_DESC_RSC,
123+
)
124+
125+
# Stop when the frontier is empty.
126+
if vector.vector_nvals(frontier) == 0:
127+
break
128+
129+
return level_vec, parent_vec
130+
131+
132+
if __name__ == "__main__":
133+
import suitesparse_graphblas as gb
134+
135+
gb.initialize()
136+
137+
# Build a small undirected graph (7 nodes):
138+
#
139+
# 0 --- 1 --- 2
140+
# | |
141+
# 3 4 --- 5
142+
# |
143+
# 6
144+
#
145+
n = 7
146+
A = matrix.matrix_new(lib.GrB_BOOL, n, n)
147+
edges = [(0, 1), (0, 3), (1, 2), (1, 4), (4, 5), (4, 6)]
148+
for i, j in edges:
149+
matrix.set_bool(A, True, i, j)
150+
matrix.set_bool(A, True, j, i) # undirected
151+
152+
src = 0
153+
level, parent = bfs(A, src)
154+
155+
print(f"BFS from node {src}:")
156+
print(f" {'node':>4} {'level':>5} {'parent':>6}")
157+
for i in range(n):
158+
lv = vector.get_int64(level, i)
159+
pa = vector.get_int64(parent, i)
160+
print(f" {i:>4} {lv:>5} {pa:>6}")

0 commit comments

Comments
 (0)