Skip to content

Commit 8937853

Browse files
authored
Add GxB serialize and deserialize for matrices and vectors. (#44)
These are thin wrappers as per typical for `python-suitesparse-graphblas`
1 parent 6998b61 commit 8937853

File tree

7 files changed

+200
-0
lines changed

7 files changed

+200
-0
lines changed

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ test=pytest
33

44
[flake8]
55
max-line-length = 100
6+
inline-quotes = "
67
exclude =
78
versioneer.py,
89
ignore =

suitesparse_graphblas/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,15 @@ def libget(name):
117117
lib.GrB_DOMAIN_MISMATCH: ex.DomainMismatch,
118118
lib.GrB_DIMENSION_MISMATCH: ex.DimensionMismatch,
119119
lib.GrB_OUTPUT_NOT_EMPTY: ex.OutputNotEmpty,
120+
lib.GrB_EMPTY_OBJECT: ex.EmptyObject,
120121
# Execution Errors
121122
lib.GrB_OUT_OF_MEMORY: ex.OutOfMemory,
122123
lib.GrB_INSUFFICIENT_SPACE: ex.InsufficientSpace,
123124
lib.GrB_INDEX_OUT_OF_BOUNDS: ex.IndexOutOfBound,
124125
lib.GrB_PANIC: ex.Panic,
126+
lib.GrB_NOT_IMPLEMENTED: ex.NotImplementedException,
127+
# GxB Errors
128+
lib.GxB_EXHAUSTED: StopIteration,
125129
}
126130
GrB_SUCCESS = lib.GrB_SUCCESS
127131
GrB_NO_VALUE = lib.GrB_NO_VALUE

suitesparse_graphblas/exceptions.py

+8
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class OutputNotEmpty(GraphBLASException):
3838
pass
3939

4040

41+
class EmptyObject(GraphBLASException):
42+
pass
43+
44+
4145
class OutOfMemory(GraphBLASException):
4246
pass
4347

@@ -52,3 +56,7 @@ class IndexOutOfBound(GraphBLASException):
5256

5357
class Panic(GraphBLASException):
5458
pass
59+
60+
61+
class NotImplementedException(GraphBLASException):
62+
pass

suitesparse_graphblas/io/serialize.py

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import numpy as np
2+
3+
from suitesparse_graphblas import check_status, ffi, lib
4+
from suitesparse_graphblas.utils import claim_buffer
5+
6+
7+
def free_desc(desc):
8+
"""Free a descriptor."""
9+
check_status(desc, lib.GrB_Descriptor_free(desc))
10+
11+
12+
def get_serialize_desc(compression=lib.GxB_COMPRESSION_DEFAULT, level=None, nthreads=None):
13+
"""Create a descriptor for serializing or deserializing.
14+
15+
This returns None (for NULL descriptor) or a pointer to a GrB_Descriptor.
16+
"""
17+
if nthreads is None and (compression is None or compression == lib.GxB_COMPRESSION_DEFAULT):
18+
return None
19+
desc = ffi.new("GrB_Descriptor*")
20+
check_status(desc, lib.GrB_Descriptor_new(desc))
21+
desc = ffi.gc(desc, free_desc)
22+
if nthreads is not None:
23+
check_status(desc, lib.GxB_Desc_set(desc[0], lib.GxB_NTHREADS, ffi.cast("int", nthreads)))
24+
if compression is not None:
25+
if level is not None and compression == lib.GxB_COMPRESSION_LZ4HC:
26+
compression += level
27+
check_status(
28+
desc, lib.GxB_Desc_set(desc[0], lib.GxB_COMPRESSION, ffi.cast("int", compression))
29+
)
30+
return desc
31+
32+
33+
def serialize_matrix(A, compression=lib.GxB_COMPRESSION_DEFAULT, level=None, *, nthreads=None):
34+
"""Serialize a Matrix into an array of bytes.
35+
36+
Parameters
37+
----------
38+
compression : int, optional
39+
One of None, GxB_COMPRESSION_NONE, GxB_COMPRESSION_DEFAULT,
40+
GxB_COMPRESSION_LZ4, or GxB_COMPRESSION_LZ4HC
41+
level : int, optional
42+
Used by GxB_COMPRESSION_LZ4HC. Should be between 1 and 9, where 9 is most compressed.
43+
nthreads : int, optional
44+
The maximum number of OpenMP threads to use.
45+
"""
46+
desc = get_serialize_desc(compression, level, nthreads)
47+
data_ptr = ffi.new("void**")
48+
size_ptr = ffi.new("GrB_Index*")
49+
check_status(
50+
A, lib.GxB_Matrix_serialize(data_ptr, size_ptr, A[0], ffi.NULL if desc is None else desc[0])
51+
)
52+
return claim_buffer(ffi, data_ptr[0], size_ptr[0], np.dtype(np.uint8))
53+
54+
55+
def serialize_vector(v, compression=lib.GxB_COMPRESSION_DEFAULT, level=None, *, nthreads=None):
56+
"""Serialize a Vector into an array of bytes.
57+
58+
Parameters
59+
----------
60+
compression : int, optional
61+
One of None, GxB_COMPRESSION_NONE, GxB_COMPRESSION_DEFAULT,
62+
GxB_COMPRESSION_LZ4, or GxB_COMPRESSION_LZ4HC
63+
level : int, optional
64+
Used by GxB_COMPRESSION_LZ4HC. Should be between 1 and 9, where 9 is most compressed.
65+
nthreads : int, optional
66+
The maximum number of OpenMP threads to use.
67+
"""
68+
desc = get_serialize_desc(compression, level, nthreads)
69+
data_ptr = ffi.new("void**")
70+
size_ptr = ffi.new("GrB_Index*")
71+
check_status(
72+
v, lib.GxB_Vector_serialize(data_ptr, size_ptr, v[0], ffi.NULL if desc is None else desc[0])
73+
)
74+
return claim_buffer(ffi, data_ptr[0], size_ptr[0], np.dtype(np.uint8))
75+
76+
77+
def deserialize_matrix(data, *, free=True, nthreads=None):
78+
"""Deserialize a Matrix from bytes.
79+
80+
The `free` argument is called when the object is garbage
81+
collected, the default is `matrix.free()`. If `free` is None then
82+
there is no automatic garbage collection and it is up to the user
83+
to free the matrix.
84+
"""
85+
data = np.frombuffer(data, np.uint8)
86+
desc = get_serialize_desc(None, nthreads)
87+
A = ffi.new("GrB_Matrix*")
88+
check_status(
89+
A,
90+
lib.GxB_Matrix_deserialize(
91+
A,
92+
ffi.NULL, # dtype; we don't check for now
93+
ffi.from_buffer("void*", data),
94+
data.nbytes,
95+
ffi.NULL if desc is None else desc[0],
96+
),
97+
)
98+
if free:
99+
if callable(free):
100+
return ffi.gc(A, free)
101+
return ffi.gc(A, matrix.free)
102+
return A
103+
104+
105+
def deserialize_vector(data, *, free=True, nthreads=None):
106+
"""Deserialize a Vector from bytes.
107+
108+
The `free` argument is called when the object is garbage
109+
collected, the default is `vector.free()`. If `free` is None then
110+
there is no automatic garbage collection and it is up to the user
111+
to free the vector.
112+
"""
113+
data = np.frombuffer(data, np.uint8)
114+
desc = get_serialize_desc(None, nthreads)
115+
v = ffi.new("GrB_Vector*")
116+
check_status(
117+
v,
118+
lib.GxB_Vector_deserialize(
119+
v,
120+
ffi.NULL, # dtype; we don't check for now
121+
ffi.from_buffer("void*", data),
122+
data.nbytes,
123+
ffi.NULL if desc is None else desc[0],
124+
),
125+
)
126+
if free:
127+
if callable(free):
128+
return ffi.gc(v, free)
129+
return ffi.gc(v, vector.free)
130+
return v
131+
132+
133+
from suitesparse_graphblas import matrix, vector # noqa isort:skip

suitesparse_graphblas/matrix.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from suitesparse_graphblas import check_status, ffi, lib
22

3+
from .io.serialize import deserialize_matrix as deserialize # noqa
4+
from .io.serialize import serialize_matrix as serialize # noqa
5+
36

47
def free(A):
58
"""Free a matrix."""

suitesparse_graphblas/tests/test_io.py

+48
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
signed_integer_types,
1919
supports_complex,
2020
unsigned_integer_types,
21+
vector,
2122
)
2223

2324
if platform.system() == "Windows":
@@ -87,6 +88,53 @@ def _test_elements(T):
8788
)
8889

8990

91+
def test_serialize_matrix():
92+
T = lib.GrB_INT64
93+
A = matrix.new(T, 2, 2)
94+
for args in zip(*_test_elements(T)):
95+
f = _element_setters[T]
96+
check_status(A, f(A[0], *args))
97+
data = matrix.serialize(A)
98+
B = matrix.deserialize(data)
99+
100+
# Test equal
101+
C = matrix.new(lib.GrB_BOOL, 2, 2)
102+
check_status(
103+
C,
104+
lib.GrB_Matrix_eWiseAdd_BinaryOp(C[0], NULL, NULL, _eq_ops[T], A[0], B[0], NULL),
105+
)
106+
assert matrix.nvals(A) == matrix.nvals(B) == matrix.nvals(C)
107+
is_eq = ffi.new("bool*")
108+
check_status(
109+
C,
110+
lib.GrB_Matrix_reduce_BOOL(is_eq, NULL, lib.GrB_LAND_MONOID_BOOL, C[0], NULL),
111+
)
112+
assert is_eq[0]
113+
114+
115+
def test_serialize_vector():
116+
T = lib.GrB_INT64
117+
v = vector.new(T, 3)
118+
check_status(v, lib.GrB_Vector_setElement_INT64(v[0], 2, 0))
119+
check_status(v, lib.GrB_Vector_setElement_INT64(v[0], 10, 1))
120+
data = vector.serialize(v, lib.GxB_COMPRESSION_LZ4HC, level=7)
121+
w = vector.deserialize(data)
122+
123+
# Test equal
124+
x = vector.new(lib.GrB_BOOL, 3)
125+
check_status(
126+
x,
127+
lib.GrB_Vector_eWiseAdd_BinaryOp(x[0], NULL, NULL, _eq_ops[T], v[0], w[0], NULL),
128+
)
129+
assert vector.nvals(v) == vector.nvals(w) == vector.nvals(x)
130+
is_eq = ffi.new("bool*")
131+
check_status(
132+
x,
133+
lib.GrB_Vector_reduce_BOOL(is_eq, NULL, lib.GrB_LAND_MONOID_BOOL, x[0], NULL),
134+
)
135+
assert is_eq[0]
136+
137+
90138
def test_matrix_binfile_read_write(tmp_path):
91139
for opener in (Path.open, gzip.open, bz2.open, lzma.open):
92140
for format in (lib.GxB_BY_ROW, lib.GxB_BY_COL):

suitesparse_graphblas/vector.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from suitesparse_graphblas import check_status, ffi, lib
22

3+
from .io.serialize import deserialize_vector as deserialize # noqa
4+
from .io.serialize import serialize_vector as serialize # noqa
5+
36

47
def free(v):
58
"""Free a vector."""

0 commit comments

Comments
 (0)