diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 56200cd88..434e6bbc6 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -65,7 +65,7 @@ jobs: if [[ ${{ matrix.cfg.sourcetype }} == "wheel" ]]; then pip install suitesparse-graphblas else - conda install -c conda-forge "graphblas>=6" + conda install -c conda-forge "graphblas>=7.0.2" fi if [[ ${{ matrix.cfg.sourcetype }} == "source" ]]; then pip install --no-binary=all suitesparse-graphblas diff --git a/grblas/_ss/matrix.py b/grblas/_ss/matrix.py index ad5db5bce..d3eee1c46 100644 --- a/grblas/_ss/matrix.py +++ b/grblas/_ss/matrix.py @@ -11,7 +11,7 @@ from .. import ffi, lib, monoid from ..base import call, record_raw from ..dtypes import _INDEX, INT64, lookup_dtype -from ..exceptions import check_status, check_status_carg +from ..exceptions import _error_code_lookup, check_status, check_status_carg from ..scalar import Scalar, _as_scalar from ..utils import ( _CArray, @@ -26,6 +26,7 @@ from .utils import get_order ffi_new = ffi.new +NULL = ffi.NULL @njit @@ -354,7 +355,8 @@ def is_iso(self): @property def iso_value(self): if self.is_iso: - return self._parent.reduce_scalar(monoid.any).new(name="") + # This may not be thread-safe if the parent is being modified in another thread + return Scalar.from_value(next(self.itervalues()), dtype=self._parent.dtype, name="") raise ValueError("Matrix is not iso-valued") @property @@ -574,6 +576,120 @@ def build_scalar(self, rows, columns, value): ], ) + def _begin_iter(self, seek): + it_ptr = ffi.new("GxB_Iterator*") + info = lib.GxB_Iterator_new(it_ptr) + it = it_ptr[0] + success = lib.GrB_SUCCESS + info = lib.GxB_Matrix_Iterator_attach(it, self._parent._carg, NULL) + if info != success: # pragma: no cover + lib.GxB_Iterator_free(it_ptr) + raise _error_code_lookup[info]("Matrix iterator failed to attach") + if seek < 0: + p = lib.GxB_Matrix_Iterator_getpmax(it) + seek += p + if seek < 0: + seek = 0 + info = lib.GxB_Matrix_Iterator_seek(it, seek) + if info != success: + lib.GxB_Iterator_free(it_ptr) + raise _error_code_lookup[info]("Matrix iterator failed to seek") + return it_ptr + + def iterkeys(self, seek=0): + """Iterate over all the row and column indices of a Matrix. + + Parameters + ---------- + seek : int, default 0 + Index of entry to seek to. May be negative to seek backwards from the end. + Matrix objects in bitmap format seek as if it's full format (i.e., it + ignores the bitmap mask). + + The Matrix should not be modified during iteration; doing so will + result in undefined behavior. + + """ + try: + it_ptr = self._begin_iter(seek) + except StopIteration: + return + it = it_ptr[0] + info = success = lib.GrB_SUCCESS + key_func = lib.GxB_Matrix_Iterator_getIndex + next_func = lib.GxB_Matrix_Iterator_next + row_ptr = ffi_new("GrB_Index*") + col_ptr = ffi_new("GrB_Index*") + while info == success: + key_func(it, row_ptr, col_ptr) + yield (row_ptr[0], col_ptr[0]) + info = next_func(it) + lib.GxB_Iterator_free(it_ptr) + if info != lib.GxB_EXHAUSTED: # pragma: no cover + raise _error_code_lookup[info]("Matrix iterator failed") + + def itervalues(self, seek=0): + """Iterate over all the values of a Matrix. + + Parameters + ---------- + seek : int, default 0 + Index of entry to seek to. May be negative to seek backwards from the end. + Matrix objects in bitmap format seek as if it's full format (i.e., it + ignores the bitmap mask). + + The Matrix should not be modified during iteration; doing so will + result in undefined behavior. + + """ + try: + it_ptr = self._begin_iter(seek) + except StopIteration: + return + it = it_ptr[0] + info = success = lib.GrB_SUCCESS + val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") + next_func = lib.GxB_Matrix_Iterator_next + while info == success: + yield val_func(it) + info = next_func(it) + lib.GxB_Iterator_free(it_ptr) + if info != lib.GxB_EXHAUSTED: # pragma: no cover + raise _error_code_lookup[info]("Matrix iterator failed") + + def iteritems(self, seek=0): + """Iterate over all the row, column, and value triples of a Matrix. + + Parameters + ---------- + seek : int, default 0 + Index of entry to seek to. May be negative to seek backwards from the end. + Matrix objects in bitmap format seek as if it's full format (i.e., it + ignores the bitmap mask). + + The Matrix should not be modified during iteration; doing so will + result in undefined behavior. + + """ + try: + it_ptr = self._begin_iter(seek) + except StopIteration: + return + it = it_ptr[0] + info = success = lib.GrB_SUCCESS + key_func = lib.GxB_Matrix_Iterator_getIndex + val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") + next_func = lib.GxB_Matrix_Iterator_next + row_ptr = ffi_new("GrB_Index*") + col_ptr = ffi_new("GrB_Index*") + while info == success: + key_func(it, row_ptr, col_ptr) + yield (row_ptr[0], col_ptr[0], val_func(it)) + info = next_func(it) + lib.GxB_Iterator_free(it_ptr) + if info != lib.GxB_EXHAUSTED: # pragma: no cover + raise _error_code_lookup[info]("Matrix iterator failed") + def export(self, format=None, *, sort=False, give_ownership=False, raw=False): """ GxB_Matrix_export_xxx @@ -844,7 +960,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m if give_ownership: if method == "export": parent.__del__() - parent.gb_obj = ffi.NULL + parent.gb_obj = NULL else: parent.clear() elif format == "coor": @@ -883,7 +999,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ap_size = ffi_new("GrB_Index*") Ax_size = ffi_new("GrB_Index*") if sort: - jumbled = ffi.NULL + jumbled = NULL else: jumbled = ffi_new("bool*") is_iso = ffi_new("bool*") @@ -903,7 +1019,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ax_size, is_iso, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -945,7 +1061,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ax_size, is_iso, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -993,7 +1109,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m is_iso, nvec, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -1048,7 +1164,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m is_iso, nvec, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -1100,7 +1216,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ax_size, is_iso, nvals_, - ffi.NULL, + NULL, ), parent, ) @@ -1150,7 +1266,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ax, Ax_size, is_iso, - ffi.NULL, + NULL, ), parent, ) @@ -1176,7 +1292,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m rv["format"] = format rv["values"] = values if method == "export": - parent.gb_obj = ffi.NULL + parent.gb_obj = NULL if parent.dtype._is_udt: rv["dtype"] = parent.dtype return rv @@ -1338,7 +1454,7 @@ def _import_csr( values.nbytes, is_iso, not sorted_cols, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -1513,7 +1629,7 @@ def _import_csc( values.nbytes, is_iso, not sorted_rows, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -1709,7 +1825,7 @@ def _import_hypercsr( is_iso, nvec, not sorted_cols, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -1905,7 +2021,7 @@ def _import_hypercsc( is_iso, nvec, not sorted_rows, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -2085,7 +2201,7 @@ def _import_bitmapr( values.nbytes, is_iso, nvals, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -2263,7 +2379,7 @@ def _import_bitmapc( values.nbytes, is_iso, nvals, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -2413,7 +2529,7 @@ def _import_fullr( Ax, values.nbytes, is_iso, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -2561,7 +2677,7 @@ def _import_fullc( Ax, values.nbytes, is_iso, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( diff --git a/grblas/_ss/vector.py b/grblas/_ss/vector.py index b78d6c9ec..b1e773704 100644 --- a/grblas/_ss/vector.py +++ b/grblas/_ss/vector.py @@ -9,14 +9,15 @@ from .. import ffi, lib, monoid from ..base import call from ..dtypes import _INDEX, INT64, UINT64, lookup_dtype -from ..exceptions import check_status, check_status_carg -from ..scalar import _as_scalar +from ..exceptions import _error_code_lookup, check_status, check_status_carg +from ..scalar import Scalar, _as_scalar from ..utils import _CArray, ints_to_numpy_buffer, libget, values_to_numpy_buffer, wrapdoc from .matrix import MatrixArray, _concat_mn, normalize_chunks from .prefix_scan import prefix_scan from .utils import get_order ffi_new = ffi.new +NULL = ffi.NULL @njit @@ -98,7 +99,8 @@ def is_iso(self): @property def iso_value(self): if self.is_iso: - return self._parent.reduce(monoid.any).new(name="") + # This may not be thread-safe if the parent is being modified in another thread + return Scalar.from_value(next(self.itervalues()), dtype=self._parent.dtype, name="") raise ValueError("Vector is not iso-valued") @property @@ -275,6 +277,114 @@ def build_scalar(self, indices, value): ], ) + def _begin_iter(self, seek): + it_ptr = ffi.new("GxB_Iterator*") + info = lib.GxB_Iterator_new(it_ptr) + it = it_ptr[0] + success = lib.GrB_SUCCESS + info = lib.GxB_Vector_Iterator_attach(it, self._parent._carg, NULL) + if info != success: # pragma: no cover + lib.GxB_Iterator_free(it_ptr) + raise _error_code_lookup[info]("Vector iterator failed to attach") + if seek < 0: + p = lib.GxB_Vector_Iterator_getpmax(it) + seek += p + if seek < 0: + seek = 0 + info = lib.GxB_Vector_Iterator_seek(it, seek) + if info != success: + lib.GxB_Iterator_free(it_ptr) + raise _error_code_lookup[info]("Vector iterator failed to seek") + return it_ptr + + def iterkeys(self, seek=0): + """Iterate over all the indices of a Vector. + + Parameters + ---------- + seek : int, default 0 + Index of entry to seek to. May be negative to seek backwards from the end. + Vector objects in bitmap format seek as if it's full format (i.e., it + ignores the bitmap mask). + + The Vector should not be modified during iteration; doing so will + result in undefined behavior. + + """ + try: + it_ptr = self._begin_iter(seek) + except StopIteration: + return + it = it_ptr[0] + info = success = lib.GrB_SUCCESS + key_func = lib.GxB_Vector_Iterator_getIndex + next_func = lib.GxB_Vector_Iterator_next + while info == success: + yield key_func(it) + info = next_func(it) + lib.GxB_Iterator_free(it_ptr) + if info != lib.GxB_EXHAUSTED: # pragma: no cover + raise _error_code_lookup[info]("Vector iterator failed") + + def itervalues(self, seek=0): + """Iterate over all the values of a Vector. + + Parameters + ---------- + seek : int, default 0 + Index of entry to seek to. May be negative to seek backwards from the end. + Vector objects in bitmap format seek as if it's full format (i.e., it + ignores the bitmap mask). + + The Vector should not be modified during iteration; doing so will + result in undefined behavior. + + """ + try: + it_ptr = self._begin_iter(seek) + except StopIteration: + return + it = it_ptr[0] + info = success = lib.GrB_SUCCESS + val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") + next_func = lib.GxB_Vector_Iterator_next + while info == success: + yield val_func(it) + info = next_func(it) + lib.GxB_Iterator_free(it_ptr) + if info != lib.GxB_EXHAUSTED: # pragma: no cover + raise _error_code_lookup[info]("Vector iterator failed") + + def iteritems(self, seek=0): + """Iterate over all the indices and values of a Vector. + + Parameters + ---------- + seek : int, default 0 + Index of entry to seek to. May be negative to seek backwards from the end. + Vector objects in bitmap format seek as if it's full format (i.e., it + ignores the bitmap mask). + + The Vector should not be modified during iteration; doing so will + result in undefined behavior. + + """ + try: + it_ptr = self._begin_iter(seek) + except StopIteration: + return + it = it_ptr[0] + info = success = lib.GrB_SUCCESS + key_func = lib.GxB_Vector_Iterator_getIndex + val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") + next_func = lib.GxB_Vector_Iterator_next + while info == success: + yield (key_func(it), val_func(it)) + info = next_func(it) + lib.GxB_Iterator_free(it_ptr) + if info != lib.GxB_EXHAUSTED: # pragma: no cover + raise _error_code_lookup[info]("Vector iterator failed") + def export(self, format=None, *, sort=False, give_ownership=False, raw=False): """ GxB_Vextor_export_xxx @@ -382,7 +492,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m vx = ffi_new("void**") vx_size = ffi_new("GrB_Index*") if sort: - jumbled = ffi.NULL + jumbled = NULL else: jumbled = ffi_new("bool*") is_iso = ffi_new("bool*") @@ -401,7 +511,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m is_iso, nvals, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -431,7 +541,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m nvals = ffi_new("GrB_Index*") check_status( libget(f"GxB_Vector_{method}_Bitmap")( - vhandle, *args, vb, vx, vb_size, vx_size, is_iso, nvals, ffi.NULL + vhandle, *args, vb, vx, vb_size, vx_size, is_iso, nvals, NULL ), parent, ) @@ -456,7 +566,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m rv["size"] = size elif format == "full": check_status( - libget(f"GxB_Vector_{method}_Full")(vhandle, *args, vx, vx_size, is_iso, ffi.NULL), + libget(f"GxB_Vector_{method}_Full")(vhandle, *args, vx, vx_size, is_iso, NULL), parent, ) is_iso = is_iso[0] @@ -480,7 +590,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m values=values, ) if method == "export": - parent.gb_obj = ffi.NULL + parent.gb_obj = NULL if parent.dtype._is_udt: rv["dtype"] = parent.dtype return rv @@ -817,7 +927,7 @@ def _import_sparse( is_iso, nvals, not sorted_index, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -987,7 +1097,7 @@ def _import_bitmap( values.nbytes, is_iso, nvals, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -1128,7 +1238,7 @@ def _import_full( vx, values.nbytes, is_iso, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( diff --git a/grblas/exceptions.py b/grblas/exceptions.py index b4558178b..6e41b5053 100644 --- a/grblas/exceptions.py +++ b/grblas/exceptions.py @@ -90,6 +90,8 @@ class UdfParseError(GrblasException): lib.GrB_INDEX_OUT_OF_BOUNDS: IndexOutOfBound, lib.GrB_PANIC: Panic, lib.GrB_NOT_IMPLEMENTED: NotImplementedException, + # GxB Errors + lib.GxB_EXHAUSTED: StopIteration, } GrB_SUCCESS = lib.GrB_SUCCESS GrB_NO_VALUE = lib.GrB_NO_VALUE diff --git a/grblas/tests/test_matrix.py b/grblas/tests/test_matrix.py index 7d0d6de86..2711520a1 100644 --- a/grblas/tests/test_matrix.py +++ b/grblas/tests/test_matrix.py @@ -3131,6 +3131,33 @@ def test_delete_via_scalar(A): assert A.nvals == 0 +def test_iteration(A): + B = Matrix.new(int, 2, 2) + assert list(B.ss.iterkeys()) == [] + assert list(B.ss.itervalues()) == [] + assert list(B.ss.iteritems()) == [] + rows, columns, values = A.to_values() + assert sorted(zip(rows, columns)) == sorted(A.ss.iterkeys()) + assert sorted(values) == sorted(A.ss.itervalues()) + assert sorted(zip(rows, columns, values)) == sorted(A.ss.iteritems()) + N = rows.size + + A = Matrix.ss.import_bitmapr(**A.ss.export("bitmapr")) + assert A.ss.format == "bitmapr" + assert len(list(A.ss.iterkeys(3))) == N - A[0, :3].new().nvals + assert len(list(A.ss.iterkeys(-3))) == A[-1, -3:].new().nvals + + A = Matrix.ss.import_csr(**A.ss.export("csr")) + assert A.ss.format == "csr" + assert len(list(A.ss.iterkeys(3))) == N - 3 + assert len(list(A.ss.iterkeys(-3))) == 3 + assert len(list(A.ss.itervalues(N))) == 0 + assert len(list(A.ss.iteritems(N + 1))) == 0 + assert len(list(A.ss.iterkeys(N + 2))) == 0 + assert len(list(A.ss.iterkeys(-N))) == N + assert len(list(A.ss.itervalues(-N - 1))) == N + + def test_udt(): record_dtype = np.dtype([("x", np.bool_), ("y", np.float64)], align=True) udt = dtypes.register_anonymous(record_dtype, "MatrixUDT") diff --git a/grblas/tests/test_vector.py b/grblas/tests/test_vector.py index 30965d3a4..661c3b0ae 100644 --- a/grblas/tests/test_vector.py +++ b/grblas/tests/test_vector.py @@ -1828,6 +1828,34 @@ def test_infix_outer(): assert w.reduce(binary.plus[int]).new() == 2 +def test_iteration(v): + w = Vector.new(int, 2) + assert list(w.ss.iterkeys()) == [] + assert list(w.ss.itervalues()) == [] + assert list(w.ss.iteritems()) == [] + + indices, values = v.to_values() + # This is what I would expect + assert sorted(indices) == sorted(v.ss.iterkeys()) + assert sorted(values) == sorted(v.ss.itervalues()) + assert sorted(zip(indices, values)) == sorted(v.ss.iteritems()) + + N = indices.size + v = Vector.ss.import_bitmap(**v.ss.export("bitmap")) + assert v.ss.format == "bitmap" + assert len(list(v.ss.iterkeys(4))) == 2 + assert len(list(v.ss.itervalues(-3))) == 2 + assert len(list(v.ss.iteritems(-v.size))) == N + assert len(list(v.ss.itervalues(-v.size - 1))) == N + assert len(list(v.ss.iterkeys(v.size + 1))) == 0 + + v = Vector.ss.import_sparse(**v.ss.export("sparse")) + assert v.ss.format == "sparse" + assert len(list(v.ss.iterkeys(2))) == 2 + assert len(list(v.ss.itervalues(N))) == 0 + assert len(list(v.ss.iteritems(N + 1))) == 0 + + def test_broadcasting(A, v): # Vector on left expected = A.dup() diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 554c36617..a967ad621 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "grblas" %} -{% set version = "2022.3.0" %} +{% set version = "2022.4.1" %} package: name: "{{ name|lower }}" @@ -21,7 +21,7 @@ requirements: run: - python - numba - - python-suitesparse-graphblas >=6.0,<6.3 + - python-suitesparse-graphblas >=7.0.2,<7.1 - pytest-runner about: diff --git a/setup.py b/setup.py index 221d65750..945e0b837 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ url="https://github.com/metagraph-dev/grblas", packages=find_packages(), python_requires=">=3.8", - install_requires=["suitesparse-graphblas >=6.0, <6.3", "numba", "donfig", "pyyaml"], + install_requires=["suitesparse-graphblas >=7.0.2, <7.1", "numba", "donfig", "pyyaml"], tests_require=["pytest", "pandas"], extras_require=extras_require, include_package_data=True,