From c862538f5f011a94821f4e81c7adeb0ce9801566 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 5 Apr 2022 16:11:06 -0500 Subject: [PATCH 1/4] Add `x.ss.iter{keys,values,items}` I've been sitting on this for a while, so I thought I'd push this up. I still need to read the documentation more closely. Also, SuiteSparse:GraphBLAS provides even more iteration functionality. --- grblas/_ss/matrix.py | 106 +++++++++++++++++++++++++++++------- grblas/_ss/vector.py | 82 ++++++++++++++++++++++++---- grblas/exceptions.py | 2 + grblas/tests/test_matrix.py | 11 ++++ grblas/tests/test_vector.py | 22 ++++++++ 5 files changed, 194 insertions(+), 29 deletions(-) diff --git a/grblas/_ss/matrix.py b/grblas/_ss/matrix.py index 7c958ee2e..a87536dd5 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,7 @@ def is_iso(self): @property def iso_value(self): if self.is_iso: - return self._parent.reduce_scalar(monoid.any).new(name="") + return next(self.itervalues()) raise ValueError("Matrix is not iso-valued") @property @@ -574,6 +575,73 @@ def build_scalar(self, rows, columns, value): ], ) + def _begin_iter(self): + 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") + info = lib.GxB_Matrix_Iterator_seek(it, 0) + if info != success: # pragma: no cover + lib.GxB_Iterator_free(it_ptr) + raise _error_code_lookup[info]("Matrix iterator failed to seek") + return it_ptr + + def iterkeys(self): + if self._parent._nvals == 0: + return + it_ptr = self._begin_iter() + 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): + if self._parent._nvals == 0: + return + it_ptr = self._begin_iter() + 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): + if self._parent._nvals == 0: + return + it_ptr = self._begin_iter() + 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 +912,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() return rv @@ -881,7 +949,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*") @@ -901,7 +969,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ax_size, is_iso, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -943,7 +1011,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ax_size, is_iso, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -991,7 +1059,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m is_iso, nvec, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -1046,7 +1114,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m is_iso, nvec, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -1098,7 +1166,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ax_size, is_iso, nvals_, - ffi.NULL, + NULL, ), parent, ) @@ -1148,7 +1216,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m Ax, Ax_size, is_iso, - ffi.NULL, + NULL, ), parent, ) @@ -1174,7 +1242,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 return rv @classmethod @@ -1334,7 +1402,7 @@ def _import_csr( values.nbytes, is_iso, not sorted_cols, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -1509,7 +1577,7 @@ def _import_csc( values.nbytes, is_iso, not sorted_rows, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -1705,7 +1773,7 @@ def _import_hypercsr( is_iso, nvec, not sorted_cols, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -1901,7 +1969,7 @@ def _import_hypercsc( is_iso, nvec, not sorted_rows, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -2081,7 +2149,7 @@ def _import_bitmapr( values.nbytes, is_iso, nvals, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -2259,7 +2327,7 @@ def _import_bitmapc( values.nbytes, is_iso, nvals, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -2409,7 +2477,7 @@ def _import_fullr( Ax, values.nbytes, is_iso, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -2557,7 +2625,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 feb7a8c1a..1643fce72 100644 --- a/grblas/_ss/vector.py +++ b/grblas/_ss/vector.py @@ -9,7 +9,7 @@ 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 ..exceptions import _error_code_lookup, check_status, check_status_carg from ..scalar import _as_scalar from ..utils import _CArray, ints_to_numpy_buffer, libget, values_to_numpy_buffer, wrapdoc from .matrix import MatrixArray, _concat_mn, normalize_chunks @@ -17,6 +17,7 @@ from .utils import get_order ffi_new = ffi.new +NULL = ffi.NULL @njit @@ -98,7 +99,7 @@ def is_iso(self): @property def iso_value(self): if self.is_iso: - return self._parent.reduce(monoid.any).new(name="") + return next(self.itervalues()) raise ValueError("Vector is not iso-valued") @property @@ -275,6 +276,67 @@ def build_scalar(self, indices, value): ], ) + def _begin_iter(self): + 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") + info = lib.GxB_Vector_Iterator_seek(it, 0) + if info != success: # pragma: no cover + lib.GxB_Iterator_free(it_ptr) + raise _error_code_lookup[info]("Vector iterator failed to seek") + return it_ptr + + def iterkeys(self): + if self._parent._nvals == 0: + return + it_ptr = self._begin_iter() + 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): + if self._parent._nvals == 0: + return + it_ptr = self._begin_iter() + 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): + if self._parent._nvals == 0: + return + it_ptr = self._begin_iter() + 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 +444,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 +463,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m is_iso, nvals, jumbled, - ffi.NULL, + NULL, ), parent, ) @@ -431,7 +493,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 +518,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 +542,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 return rv @classmethod @@ -815,7 +877,7 @@ def _import_sparse( is_iso, nvals, not sorted_index, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -985,7 +1047,7 @@ def _import_bitmap( values.nbytes, is_iso, nvals, - ffi.NULL, + NULL, ) if method == "import": check_status_carg( @@ -1126,7 +1188,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 9149b2f5a..18a182691 100644 --- a/grblas/tests/test_matrix.py +++ b/grblas/tests/test_matrix.py @@ -3121,3 +3121,14 @@ def test_delete_via_scalar(A): assert A[:, 0].new().nvals == 0 del 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()) diff --git a/grblas/tests/test_vector.py b/grblas/tests/test_vector.py index 8cc512e3b..2e0701f85 100644 --- a/grblas/tests/test_vector.py +++ b/grblas/tests/test_vector.py @@ -1715,3 +1715,25 @@ def test_infix_outer(): w[:] = False w |= True 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 + with pytest.raises(Exception): + assert sorted(indices) == sorted(v.ss.iterkeys()) + with pytest.raises(Exception): + assert sorted(values) == sorted(v.ss.itervalues()) + with pytest.raises(Exception): + assert sorted(zip(indices, values)) == sorted(v.ss.iteritems()) + + # But apparently bitmap give us everything (?) + # See https://github.com/DrTimothyAldenDavis/GraphBLAS/issues/129 + assert len(indices) < len(list(v.ss.iterkeys())) + assert len(values) < len(list(v.ss.itervalues())) + assert len(values) < len(list(v.ss.iteritems())) From 62e8fbdf6a7307f4db1923751971d788897860ce Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 9 Apr 2022 20:53:09 -0500 Subject: [PATCH 2/4] Update min required graphblas version --- .github/workflows/test_and_build.yml | 2 +- grblas/_ss/matrix.py | 1 + grblas/_ss/vector.py | 1 + grblas/tests/test_vector.py | 15 +++------------ recipe/meta.yaml | 4 ++-- setup.py | 2 +- 6 files changed, 9 insertions(+), 16 deletions(-) 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 019ea2d39..756c21236 100644 --- a/grblas/_ss/matrix.py +++ b/grblas/_ss/matrix.py @@ -355,6 +355,7 @@ def is_iso(self): @property def iso_value(self): if self.is_iso: + # This may not be thread-safe if the parent is being modified in another thread return next(self.itervalues()) raise ValueError("Matrix is not iso-valued") diff --git a/grblas/_ss/vector.py b/grblas/_ss/vector.py index 28ced8c71..d6eeb9358 100644 --- a/grblas/_ss/vector.py +++ b/grblas/_ss/vector.py @@ -99,6 +99,7 @@ def is_iso(self): @property def iso_value(self): if self.is_iso: + # This may not be thread-safe if the parent is being modified in another thread return next(self.itervalues()) raise ValueError("Vector is not iso-valued") diff --git a/grblas/tests/test_vector.py b/grblas/tests/test_vector.py index 9fef05568..55958ca21 100644 --- a/grblas/tests/test_vector.py +++ b/grblas/tests/test_vector.py @@ -1836,18 +1836,9 @@ def test_iteration(v): indices, values = v.to_values() # This is what I would expect - with pytest.raises(Exception): - assert sorted(indices) == sorted(v.ss.iterkeys()) - with pytest.raises(Exception): - assert sorted(values) == sorted(v.ss.itervalues()) - with pytest.raises(Exception): - assert sorted(zip(indices, values)) == sorted(v.ss.iteritems()) - - # But apparently bitmap give us everything (?) - # See https://github.com/DrTimothyAldenDavis/GraphBLAS/issues/129 - assert len(indices) < len(list(v.ss.iterkeys())) - assert len(values) < len(list(v.ss.itervalues())) - assert len(values) < len(list(v.ss.iteritems())) + assert sorted(indices) == sorted(v.ss.iterkeys()) + assert sorted(values) == sorted(v.ss.itervalues()) + assert sorted(zip(indices, values)) == sorted(v.ss.iteritems()) def test_broadcasting(A, v): 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, From 203b17f8c80615dd9ba40f5b2d4983876908a84d Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 11 Apr 2022 12:44:12 -0500 Subject: [PATCH 3/4] Add seek argument to SuiteSparse iterators, and add docstrings. --- grblas/_ss/matrix.py | 71 ++++++++++++++++++++++++++++++------- grblas/_ss/vector.py | 71 ++++++++++++++++++++++++++++++------- grblas/tests/test_matrix.py | 16 +++++++++ grblas/tests/test_vector.py | 14 ++++++++ 4 files changed, 148 insertions(+), 24 deletions(-) diff --git a/grblas/_ss/matrix.py b/grblas/_ss/matrix.py index 756c21236..1d6ee554f 100644 --- a/grblas/_ss/matrix.py +++ b/grblas/_ss/matrix.py @@ -576,7 +576,7 @@ def build_scalar(self, rows, columns, value): ], ) - def _begin_iter(self): + def _begin_iter(self, seek): it_ptr = ffi.new("GxB_Iterator*") info = lib.GxB_Iterator_new(it_ptr) it = it_ptr[0] @@ -585,16 +585,35 @@ def _begin_iter(self): if info != success: # pragma: no cover lib.GxB_Iterator_free(it_ptr) raise _error_code_lookup[info]("Matrix iterator failed to attach") - info = lib.GxB_Matrix_Iterator_seek(it, 0) - if info != success: # pragma: no cover + 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): - if self._parent._nvals == 0: + 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_ptr = self._begin_iter() it = it_ptr[0] info = success = lib.GrB_SUCCESS key_func = lib.GxB_Matrix_Iterator_getIndex @@ -609,10 +628,24 @@ def iterkeys(self): if info != lib.GxB_EXHAUSTED: # pragma: no cover raise _error_code_lookup[info]("Matrix iterator failed") - def itervalues(self): - if self._parent._nvals == 0: + 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_ptr = self._begin_iter() it = it_ptr[0] info = success = lib.GrB_SUCCESS val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") @@ -624,10 +657,24 @@ def itervalues(self): if info != lib.GxB_EXHAUSTED: # pragma: no cover raise _error_code_lookup[info]("Matrix iterator failed") - def iteritems(self): - if self._parent._nvals == 0: + 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_ptr = self._begin_iter() it = it_ptr[0] info = success = lib.GrB_SUCCESS key_func = lib.GxB_Matrix_Iterator_getIndex diff --git a/grblas/_ss/vector.py b/grblas/_ss/vector.py index d6eeb9358..138048de5 100644 --- a/grblas/_ss/vector.py +++ b/grblas/_ss/vector.py @@ -277,7 +277,7 @@ def build_scalar(self, indices, value): ], ) - def _begin_iter(self): + def _begin_iter(self, seek): it_ptr = ffi.new("GxB_Iterator*") info = lib.GxB_Iterator_new(it_ptr) it = it_ptr[0] @@ -286,16 +286,35 @@ def _begin_iter(self): if info != success: # pragma: no cover lib.GxB_Iterator_free(it_ptr) raise _error_code_lookup[info]("Vector iterator failed to attach") - info = lib.GxB_Vector_Iterator_seek(it, 0) - if info != success: # pragma: no cover + 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): - if self._parent._nvals == 0: + 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_ptr = self._begin_iter() it = it_ptr[0] info = success = lib.GrB_SUCCESS key_func = lib.GxB_Vector_Iterator_getIndex @@ -307,10 +326,24 @@ def iterkeys(self): if info != lib.GxB_EXHAUSTED: # pragma: no cover raise _error_code_lookup[info]("Vector iterator failed") - def itervalues(self): - if self._parent._nvals == 0: + 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_ptr = self._begin_iter() it = it_ptr[0] info = success = lib.GrB_SUCCESS val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") @@ -322,10 +355,24 @@ def itervalues(self): if info != lib.GxB_EXHAUSTED: # pragma: no cover raise _error_code_lookup[info]("Vector iterator failed") - def iteritems(self): - if self._parent._nvals == 0: + 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_ptr = self._begin_iter() it = it_ptr[0] info = success = lib.GrB_SUCCESS key_func = lib.GxB_Vector_Iterator_getIndex diff --git a/grblas/tests/test_matrix.py b/grblas/tests/test_matrix.py index 6c983bcb5..2711520a1 100644 --- a/grblas/tests/test_matrix.py +++ b/grblas/tests/test_matrix.py @@ -3140,6 +3140,22 @@ def test_iteration(A): 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(): diff --git a/grblas/tests/test_vector.py b/grblas/tests/test_vector.py index 55958ca21..140b7c5bc 100644 --- a/grblas/tests/test_vector.py +++ b/grblas/tests/test_vector.py @@ -1840,6 +1840,20 @@ def test_iteration(v): 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.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 From cdb451f87aa464494bc5fe58c54dac7252f119d0 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 11 Apr 2022 13:23:42 -0500 Subject: [PATCH 4/4] Change `x.ss.iso_value` to return a `Scalar`; also, coverage --- grblas/_ss/matrix.py | 2 +- grblas/_ss/vector.py | 4 ++-- grblas/tests/test_vector.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/grblas/_ss/matrix.py b/grblas/_ss/matrix.py index 1d6ee554f..d3eee1c46 100644 --- a/grblas/_ss/matrix.py +++ b/grblas/_ss/matrix.py @@ -356,7 +356,7 @@ def is_iso(self): def iso_value(self): if self.is_iso: # This may not be thread-safe if the parent is being modified in another thread - return next(self.itervalues()) + return Scalar.from_value(next(self.itervalues()), dtype=self._parent.dtype, name="") raise ValueError("Matrix is not iso-valued") @property diff --git a/grblas/_ss/vector.py b/grblas/_ss/vector.py index 138048de5..b1e773704 100644 --- a/grblas/_ss/vector.py +++ b/grblas/_ss/vector.py @@ -10,7 +10,7 @@ from ..base import call from ..dtypes import _INDEX, INT64, UINT64, lookup_dtype from ..exceptions import _error_code_lookup, check_status, check_status_carg -from ..scalar import _as_scalar +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 @@ -100,7 +100,7 @@ def is_iso(self): def iso_value(self): if self.is_iso: # This may not be thread-safe if the parent is being modified in another thread - return next(self.itervalues()) + return Scalar.from_value(next(self.itervalues()), dtype=self._parent.dtype, name="") raise ValueError("Vector is not iso-valued") @property diff --git a/grblas/tests/test_vector.py b/grblas/tests/test_vector.py index 140b7c5bc..661c3b0ae 100644 --- a/grblas/tests/test_vector.py +++ b/grblas/tests/test_vector.py @@ -1846,6 +1846,7 @@ def test_iteration(v): 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"))