From 3f316271d1d1124295c33cf9fb04f1ff26d5417e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 22 Aug 2023 22:16:56 -0500 Subject: [PATCH 01/10] Experiment with adding `A.setdiag(x, k)` --- graphblas/core/expr.py | 23 +++++++++ graphblas/core/matrix.py | 91 ++++++++++++++++++++++++++++++++++ graphblas/tests/test_matrix.py | 4 ++ 3 files changed, 118 insertions(+) diff --git a/graphblas/core/expr.py b/graphblas/core/expr.py index d803939a5..d73581617 100644 --- a/graphblas/core/expr.py +++ b/graphblas/core/expr.py @@ -476,6 +476,29 @@ def __eq__(self, other): def __bool__(self): raise TypeError(f"__bool__ not defined for objects of type {type(self)}.") + @property + def setdiag(self): + if self.parent.ndim != 2: + raise AttributeError(f"'{type(self).__name__}' object has no attribute 'setdiag'") + return self._setdiag + + def _setdiag(self, values, k=0, *, clear_missing=True): + return self.parent._setdiag( + values, + k, + clear_missing=clear_missing, + mask=self.kwargs["mask"], + accum=self.kwargs["accum"], + replace=self.kwargs["replace"], + opts=self.opts, + ) + + def __dir__(self): + rv = super().__dir__() + if self.parent.ndim != 2: + rv = list(set(rv) - {"setdiag", "_setdiag"}) + return rv + class InfixExprBase: __slots__ = "left", "right", "_expr", "__weakref__" diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index d820ca424..ab70abfbb 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2805,6 +2805,97 @@ def power(self, n, op=semiring.plus_times): dtype=self.dtype, ) + def setdiag(self, values, k=0, *, clear_missing=True, **opts): + """https://github.com/python-graphblas/python-graphblas/issues/487 + + Set k'th diagonal with a Scalar, Vector, or array. + """ + self._setdiag(values, k, clear_missing=clear_missing, opts=opts) + + def _setdiag( + self, values, k=0, *, clear_missing=True, mask=None, accum=None, replace=None, opts=None + ): + if (K := maybe_integral(k)) is None: + raise TypeError(f"k must be an integer; got bad type: {type(k)}") + k = K + if k < 0: + size = min(self._nrows + k, self._ncols) + if size < 0: + raise IndexError( + f"k={k} is too small; the k'th diagonal is out of range. " + f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " + f"{-self._nrows} <= k <= {self.ncols}" + ) + else: + size = min(self._ncols - k, self._nrows) + if size < 0: + raise IndexError( + f"k={k} is too large; the k'th diagonal is out of range. " + f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " + f"{-self._nrows} <= k <= {self.ncols}" + ) + + # Convert `values` to Vector if necessary (i.e., it's scalar or array) + is_scalar = clear_diag = False + if output_type(values) is Vector: + v = values + clear_diag = clear_missing and v._nvals != v._size + elif type(values) is Scalar: + is_scalar = True + else: + dtype = self.dtype if self.dtype._is_udt else None + try: + # Try to make it a Scalar + values = Scalar.from_value(values, dtype, is_cscalar=None, name="") + is_scalar = True + except (TypeError, ValueError): + try: + # Else try to make it a numpy array + values, dtype = values_to_numpy_buffer(values, dtype) + except Exception: + self._expect_type( + values, + (Scalar, Vector, np.ndarray), + within="setdiag", + argname="values", + extra_message="Literal scalars also accepted.", + ) + else: + v = Vector.from_dense(values, dtype=dtype) + + if is_scalar: + v = Vector.from_scalar(values, size) + elif v._size != size: + raise RuntimeError("egg, bacon, and spam") + + if mask is not None: + mask = _check_mask(mask) + if mask.complement: + raise NotImplementedError + if replace: + raise NotImplementedError + if mask.parent.ndim == 2: + if mask.parent.shape != self.shape: + raise RuntimeError("spam, egg, sausage, and spam") + M = select.diag(mask.parent, k).new() + elif mask.parent._size != size: + raise RuntimeError("spam, egg, spam, spam, bacon, and spam") + else: + M = mask.parent.diag() + if M.shape != self.shape: + M.resize(self._nrows, self._ncols) + mask = type(mask)(M) + + if clear_diag: + self(mask=mask, **opts) << select.offdiag(self, k) + + Diag = v.diag(k) + if Diag.shape != self.shape: + Diag.resize(self._nrows, self._ncols) + if mask is None: + mask = Diag.S + self(accum=accum, mask=mask, **opts) << Diag + ################################## # Extract and Assign index methods ################################## diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index fe85bb9bf..a797b8767 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2925,6 +2925,7 @@ def test_expr_is_like_matrix(A): "_parent", "_prep_for_assign", "_prep_for_extract", + "_setdiag", "_to_csx", "_update", "build", @@ -2940,6 +2941,7 @@ def test_expr_is_like_matrix(A): "from_scalar", "from_values", "resize", + "setdiag", "update", } ignore = {"__sizeof__"} @@ -2990,6 +2992,7 @@ def test_index_expr_is_like_matrix(A): "_parent", "_prep_for_assign", "_prep_for_extract", + "_setdiag", "_to_csx", "_update", "build", @@ -3005,6 +3008,7 @@ def test_index_expr_is_like_matrix(A): "from_values", "from_scalar", "resize", + "setdiag", } ignore = {"__sizeof__"} assert attrs - expr_attrs - ignore == expected, ( From f9c95c54848016dce7174ffc375daf8b6100e7e5 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 11 Sep 2023 23:37:57 +0200 Subject: [PATCH 02/10] Update setdiag to have `mask=` and `accum=` arguments; also, tests --- graphblas/core/expr.py | 23 -------- graphblas/core/matrix.py | 72 +++++++++++++--------- graphblas/tests/test_matrix.py | 105 ++++++++++++++++++++++++++++++++- 3 files changed, 145 insertions(+), 55 deletions(-) diff --git a/graphblas/core/expr.py b/graphblas/core/expr.py index d73581617..d803939a5 100644 --- a/graphblas/core/expr.py +++ b/graphblas/core/expr.py @@ -476,29 +476,6 @@ def __eq__(self, other): def __bool__(self): raise TypeError(f"__bool__ not defined for objects of type {type(self)}.") - @property - def setdiag(self): - if self.parent.ndim != 2: - raise AttributeError(f"'{type(self).__name__}' object has no attribute 'setdiag'") - return self._setdiag - - def _setdiag(self, values, k=0, *, clear_missing=True): - return self.parent._setdiag( - values, - k, - clear_missing=clear_missing, - mask=self.kwargs["mask"], - accum=self.kwargs["accum"], - replace=self.kwargs["replace"], - opts=self.opts, - ) - - def __dir__(self): - rv = super().__dir__() - if self.parent.ndim != 2: - rv = list(set(rv) - {"setdiag", "_setdiag"}) - return rv - class InfixExprBase: __slots__ = "left", "right", "_expr", "__weakref__" diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index ab70abfbb..6e6976a95 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2805,41 +2805,41 @@ def power(self, n, op=semiring.plus_times): dtype=self.dtype, ) - def setdiag(self, values, k=0, *, clear_missing=True, **opts): - """https://github.com/python-graphblas/python-graphblas/issues/487 + def setdiag(self, values, k=0, *, mask=None, accum=None, clear_missing=True, **opts): + """Set k'th diagonal with a Scalar, Vector, or array. - Set k'th diagonal with a Scalar, Vector, or array. - """ - self._setdiag(values, k, clear_missing=clear_missing, opts=opts) + TODO: finish docstring. - def _setdiag( - self, values, k=0, *, clear_missing=True, mask=None, accum=None, replace=None, opts=None - ): + Parameters + ---------- + values : + k : int, default=0 + mask : Mask, optional + accum : Monoid or BinaryOp, optional + clear_missing : bool, default=True + """ if (K := maybe_integral(k)) is None: raise TypeError(f"k must be an integer; got bad type: {type(k)}") k = K if k < 0: - size = min(self._nrows + k, self._ncols) - if size < 0: + if (size := min(self._nrows + k, self._ncols)) < 0: raise IndexError( f"k={k} is too small; the k'th diagonal is out of range. " f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " f"{-self._nrows} <= k <= {self.ncols}" ) - else: - size = min(self._ncols - k, self._nrows) - if size < 0: - raise IndexError( - f"k={k} is too large; the k'th diagonal is out of range. " - f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " - f"{-self._nrows} <= k <= {self.ncols}" - ) + elif (size := min(self._ncols - k, self._nrows)) < 0: + raise IndexError( + f"k={k} is too large; the k'th diagonal is out of range. " + f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " + f"{-self._nrows} <= k <= {self.ncols}" + ) # Convert `values` to Vector if necessary (i.e., it's scalar or array) is_scalar = clear_diag = False if output_type(values) is Vector: v = values - clear_diag = clear_missing and v._nvals != v._size + clear_diag = accum is None and clear_missing and v._nvals != v._size elif type(values) is Scalar: is_scalar = True else: @@ -2866,24 +2866,38 @@ def _setdiag( if is_scalar: v = Vector.from_scalar(values, size) elif v._size != size: - raise RuntimeError("egg, bacon, and spam") + raise DimensionMismatch( + f"Dimensions not compatible for assigning length {v._size} Vector " + f"to {k}'th diagonal of Matrix with shape {self._nrows}x{self._ncols}." + f"The Vector should be size {size}." + ) if mask is not None: mask = _check_mask(mask) - if mask.complement: - raise NotImplementedError - if replace: - raise NotImplementedError if mask.parent.ndim == 2: if mask.parent.shape != self.shape: - raise RuntimeError("spam, egg, sausage, and spam") - M = select.diag(mask.parent, k).new() + raise DimensionMismatch( + "Matrix mask in setdiag is the wrong shape; " + f"expected shape {self._nrows}x{self._ncols}, " + f"got {mask.parent._nrows}x{mask.parent._ncols}" + ) + if mask.complement: + mval = type(mask)(mask.parent.diag(k)).new() + mask = mval.S + M = mval.diag() + else: + M = select.diag(mask.parent, k).new() elif mask.parent._size != size: - raise RuntimeError("spam, egg, spam, spam, bacon, and spam") + raise DimensionMismatch( + "Vector mask in setdiag is the wrong length; " + f"expected size {size}, got size {mask.parent._size}." + ) else: + if mask.complement: + mask = mask.new().S M = mask.parent.diag() - if M.shape != self.shape: - M.resize(self._nrows, self._ncols) + if M.shape != self.shape: + M.resize(self._nrows, self._ncols) mask = type(mask)(M) if clear_diag: diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index a797b8767..04b5f7656 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2925,7 +2925,6 @@ def test_expr_is_like_matrix(A): "_parent", "_prep_for_assign", "_prep_for_extract", - "_setdiag", "_to_csx", "_update", "build", @@ -2992,7 +2991,6 @@ def test_index_expr_is_like_matrix(A): "_parent", "_prep_for_assign", "_prep_for_extract", - "_setdiag", "_to_csx", "_update", "build", @@ -3005,8 +3003,8 @@ def test_index_expr_is_like_matrix(A): "from_dense", "from_dicts", "from_edgelist", - "from_values", "from_scalar", + "from_values", "resize", "setdiag", } @@ -4397,3 +4395,104 @@ def test_power(A): B = A[:2, :3].new() with pytest.raises(DimensionMismatch): B.power(2) + + +def test_setdiag(): + A = Matrix(int, 2, 3) + A.setdiag(1) + expected = Matrix(int, 2, 3) + expected[0, 0] = 1 + expected[1, 1] = 1 + assert A.isequal(expected) + A.setdiag(Scalar.from_value(2), 2) + expected[0, 2] = 2 + assert A.isequal(expected) + A.setdiag(3, k=-1) + expected[1, 0] = 3 + assert A.isequal(expected) + # List (or array) is treated as dense + A.setdiag([10, 20], 1) + expected[0, 1] = 10 + expected[1, 2] = 20 + assert A.isequal(expected) + # size 0 diagonals (doesn't set anything, but valid call) + A.setdiag(-1, 3) + assert A.isequal(expected) + A.setdiag(-1, -2) + assert A.isequal(expected) + A.setdiag([], 3) + assert A.isequal(expected) + A.setdiag(Vector(int, 0), -2) + assert A.isequal(expected) + # Now we're out of bounds + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(-1, 4) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(-1, -3) + with pytest.raises(TypeError, match="k must be an integer"): + A.setdiag(-1, 0.5) + with pytest.raises(TypeError, match="Bad type for argument `values` in Matrix.setdiag"): + A.setdiag(object()) + with pytest.raises(DimensionMismatch, match="Dimensions not compatible"): + A.setdiag([10, 20, 30], 1) + with pytest.raises(DimensionMismatch, match="Dimensions not compatible"): + A.setdiag([10], 1) + + A = Matrix(int, 2, 2, name="A") + expected = Matrix(int, 2, 2, name="expected") + v = Vector(int, 2, name="v") + Vector(int, 2) + v[0] = 1 + A.setdiag(v) + expected[0, 0] = 1 + assert A.isequal(expected) + A.setdiag(v, accum=binary.plus) + expected[0, 0] = 2 + assert A.isequal(expected) + A.setdiag(10, mask=v.S) + expected[0, 0] = 10 + assert A.isequal(expected) + A.setdiag(10, mask=v.S, accum="+") + expected[0, 0] = 20 + assert A.isequal(expected) + # Allow mask to be a matrix + A.setdiag(10, mask=A.S, accum="+") + expected[0, 0] = 30 + assert A.isequal(expected) + # Test `clear_missing=` + A.clear() + A.setdiag(99) + A.setdiag(v, clear_missing=True) + expected[0, 0] = 1 + assert A.isequal(expected) + A.setdiag(99) + A.setdiag(v, clear_missing=False) + expected[1, 1] = 99 + assert A.isequal(expected) + # clear_missing only applies where the mask is present + A.setdiag(99) + A.setdiag(v, mask=v.S, clear_missing=True) + assert A.isequal(expected) + + # We handle complemented masks! + A.clear() + expected.clear() + A.setdiag(42, mask=~v.S) + expected[1, 1] = 42 + assert A.isequal(expected) + A.setdiag(7, mask=~A.V) + expected[0, 0] = 7 + assert A.isequal(expected) + + with pytest.raises(DimensionMismatch, match="Matrix mask in setdiag is the wrong "): + A.setdiag(9, mask=Matrix(int, 3, 3).S) + with pytest.raises(DimensionMismatch, match="Vector mask in setdiag is the wrong "): + A.setdiag(10, mask=Vector(int, 3).S) + + A.clear() + A.resize(2, 3) + expected.clear() + expected.resize(2, 3) + A.setdiag(30, mask=v.S) + expected[0, 0] = 30 + assert A.isequal(expected) From 457cd03389623bfe536f4f354fd2e246200ea87b Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 12 Sep 2023 08:08:47 +0200 Subject: [PATCH 03/10] Write docstring for `A.setdiag` --- graphblas/core/matrix.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 6e6976a95..d17175c88 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2808,15 +2808,28 @@ def power(self, n, op=semiring.plus_times): def setdiag(self, values, k=0, *, mask=None, accum=None, clear_missing=True, **opts): """Set k'th diagonal with a Scalar, Vector, or array. - TODO: finish docstring. + This is not a built-in GraphBLAS operation. It is implemented as a recipe. Parameters ---------- - values : + values : Vector or list or np.ndarray or scalar + New values to assign to the diagonal. The length of Vector and array + values must match the size of the diagonal being assigned to. k : int, default=0 + Which diagonal or off-diagonal to set. For example, set the elements + ``A[i, i+k] = values[i]``. The default, k=0, is the main diagonal. mask : Mask, optional + Vector or Matrix Mask to control which diagonal elements to set. + If it is Matrix Mask, then only the diagonal is used as the mask. accum : Monoid or BinaryOp, optional + Operator to use to combine existing diagonal values and new values. clear_missing : bool, default=True + If True, missing elements in Vector values will result in the + corresponding diagonal elements to be removed. This parameter has + no effect if values is not a Vector or if accum operator is given. + The default is True to match normal behavior for assignments, but + it requires a call to ``select.offdiag``. If clear_missing is False, + then the recipe is approximately ``D = v.diag(k) ; self(D.S) << D``. """ if (K := maybe_integral(k)) is None: raise TypeError(f"k must be an integer; got bad type: {type(k)}") From da98a68344b981573503cc2d30c46f6243574553 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 12 Sep 2023 08:24:16 +0200 Subject: [PATCH 04/10] Bump black and ruff --- .pre-commit-config.yaml | 6 +++--- scripts/check_versions.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a499e8f8..389c67c40 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,12 +61,12 @@ repos: - id: auto-walrus args: [--line-length, "100"] - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black - id: black-jupyter - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.287 + rev: v0.0.288 hooks: - id: ruff args: [--fix-only, --show-fixes] @@ -94,7 +94,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.287 + rev: v0.0.288 hooks: - id: ruff - repo: https://github.com/sphinx-contrib/sphinx-lint diff --git a/scripts/check_versions.sh b/scripts/check_versions.sh index 1a3e894a6..b5f391619 100755 --- a/scripts/check_versions.sh +++ b/scripts/check_versions.sh @@ -7,7 +7,7 @@ conda search 'numpy[channel=conda-forge]>=1.25.2' conda search 'pandas[channel=conda-forge]>=2.1.0' conda search 'scipy[channel=conda-forge]>=1.11.2' conda search 'networkx[channel=conda-forge]>=3.1' -conda search 'awkward[channel=conda-forge]>=2.4.1' +conda search 'awkward[channel=conda-forge]>=2.4.2' conda search 'sparse[channel=conda-forge]>=0.14.0' conda search 'fast_matrix_market[channel=conda-forge]>=1.7.2' conda search 'numba[channel=conda-forge]>=0.57.1' From acb006762b9a913c72c32dac996cea605630eac1 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 14 Sep 2023 13:14:58 +0200 Subject: [PATCH 05/10] Raise if assigning to diagonal of size 0 --- .pre-commit-config.yaml | 4 ++-- graphblas/core/matrix.py | 8 ++++---- graphblas/tests/test_matrix.py | 21 +++++++++++---------- pyproject.toml | 3 +++ 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 389c67c40..43ebe953c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: black - id: black-jupyter - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.288 + rev: v0.0.289 hooks: - id: ruff args: [--fix-only, --show-fixes] @@ -94,7 +94,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.288 + rev: v0.0.289 hooks: - id: ruff - repo: https://github.com/sphinx-contrib/sphinx-lint diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index d17175c88..25cb56477 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2835,17 +2835,17 @@ def setdiag(self, values, k=0, *, mask=None, accum=None, clear_missing=True, **o raise TypeError(f"k must be an integer; got bad type: {type(k)}") k = K if k < 0: - if (size := min(self._nrows + k, self._ncols)) < 0: + if (size := min(self._nrows + k, self._ncols)) <= 0: raise IndexError( f"k={k} is too small; the k'th diagonal is out of range. " f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " - f"{-self._nrows} <= k <= {self.ncols}" + f"{-self._nrows} < k < {self.ncols}" ) - elif (size := min(self._ncols - k, self._nrows)) < 0: + elif (size := min(self._ncols - k, self._nrows)) <= 0: raise IndexError( f"k={k} is too large; the k'th diagonal is out of range. " f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " - f"{-self._nrows} <= k <= {self.ncols}" + f"{-self._nrows} < k < {self.ncols}" ) # Convert `values` to Vector if necessary (i.e., it's scalar or array) diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 04b5f7656..b27a1225a 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -4415,16 +4415,17 @@ def test_setdiag(): expected[0, 1] = 10 expected[1, 2] = 20 assert A.isequal(expected) - # size 0 diagonals (doesn't set anything, but valid call) - A.setdiag(-1, 3) - assert A.isequal(expected) - A.setdiag(-1, -2) - assert A.isequal(expected) - A.setdiag([], 3) - assert A.isequal(expected) - A.setdiag(Vector(int, 0), -2) - assert A.isequal(expected) - # Now we're out of bounds + # Size 0 diagonals, which does not set anything. + # This could be valid (esp. given a size 0 vector), but let's raise for now. + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(-1, 3) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(-1, -2) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag([], 3) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(Vector(int, 0), -2) + # Now we're definitely out of bounds with pytest.raises(IndexError, match="diagonal is out of range"): A.setdiag(-1, 4) with pytest.raises(IndexError, match="diagonal is out of range"): diff --git a/pyproject.toml b/pyproject.toml index 619ce18f2..ff970cc0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -235,6 +235,9 @@ ignore-words-list = "coo,ba" # https://github.com/charliermarsh/ruff/ line-length = 100 target-version = "py39" +unfixable = [ + "F841" # unused-variable (Note: can leave useless expression) +] select = [ # Have we enabled too many checks that they'll become a nuisance? We'll see... "F", # pyflakes From 7665530f395ecdc0749fed98b22eb3ea27373656 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 15 Sep 2023 00:41:49 +0200 Subject: [PATCH 06/10] Remove `clear_missing=` from `setdiag` Use `second` as the accumulator for `clear_missing=False` behavior. --- graphblas/core/matrix.py | 11 ++--------- graphblas/tests/test_matrix.py | 9 ++++----- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 25cb56477..dc235388e 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2805,7 +2805,7 @@ def power(self, n, op=semiring.plus_times): dtype=self.dtype, ) - def setdiag(self, values, k=0, *, mask=None, accum=None, clear_missing=True, **opts): + def setdiag(self, values, k=0, *, mask=None, accum=None, **opts): """Set k'th diagonal with a Scalar, Vector, or array. This is not a built-in GraphBLAS operation. It is implemented as a recipe. @@ -2823,13 +2823,6 @@ def setdiag(self, values, k=0, *, mask=None, accum=None, clear_missing=True, **o If it is Matrix Mask, then only the diagonal is used as the mask. accum : Monoid or BinaryOp, optional Operator to use to combine existing diagonal values and new values. - clear_missing : bool, default=True - If True, missing elements in Vector values will result in the - corresponding diagonal elements to be removed. This parameter has - no effect if values is not a Vector or if accum operator is given. - The default is True to match normal behavior for assignments, but - it requires a call to ``select.offdiag``. If clear_missing is False, - then the recipe is approximately ``D = v.diag(k) ; self(D.S) << D``. """ if (K := maybe_integral(k)) is None: raise TypeError(f"k must be an integer; got bad type: {type(k)}") @@ -2852,7 +2845,7 @@ def setdiag(self, values, k=0, *, mask=None, accum=None, clear_missing=True, **o is_scalar = clear_diag = False if output_type(values) is Vector: v = values - clear_diag = accum is None and clear_missing and v._nvals != v._size + clear_diag = accum is None and v._nvals != v._size elif type(values) is Scalar: is_scalar = True else: diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index b27a1225a..a4d2ccd5c 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -4460,19 +4460,18 @@ def test_setdiag(): A.setdiag(10, mask=A.S, accum="+") expected[0, 0] = 30 assert A.isequal(expected) - # Test `clear_missing=` + # Test how to clear or not clear missing elements A.clear() A.setdiag(99) - A.setdiag(v, clear_missing=True) + A.setdiag(v) expected[0, 0] = 1 assert A.isequal(expected) A.setdiag(99) - A.setdiag(v, clear_missing=False) + A.setdiag(v, accum="second") expected[1, 1] = 99 assert A.isequal(expected) - # clear_missing only applies where the mask is present A.setdiag(99) - A.setdiag(v, mask=v.S, clear_missing=True) + A.setdiag(v, mask=v.S) assert A.isequal(expected) # We handle complemented masks! From bd37af51fc36a300fd5f790b524ad6ffade0dc2a Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 15 Sep 2023 09:33:29 +0200 Subject: [PATCH 07/10] Pass `**opts` to more calls in setdiag --- graphblas/core/matrix.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index dc235388e..898765e7a 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2867,10 +2867,10 @@ def setdiag(self, values, k=0, *, mask=None, accum=None, **opts): extra_message="Literal scalars also accepted.", ) else: - v = Vector.from_dense(values, dtype=dtype) + v = Vector.from_dense(values, dtype=dtype, **opts) if is_scalar: - v = Vector.from_scalar(values, size) + v = Vector.from_scalar(values, size, **opts) elif v._size != size: raise DimensionMismatch( f"Dimensions not compatible for assigning length {v._size} Vector " @@ -2888,11 +2888,11 @@ def setdiag(self, values, k=0, *, mask=None, accum=None, **opts): f"got {mask.parent._nrows}x{mask.parent._ncols}" ) if mask.complement: - mval = type(mask)(mask.parent.diag(k)).new() + mval = type(mask)(mask.parent.diag(k)).new(**opts) mask = mval.S M = mval.diag() else: - M = select.diag(mask.parent, k).new() + M = select.diag(mask.parent, k).new(**opts) elif mask.parent._size != size: raise DimensionMismatch( "Vector mask in setdiag is the wrong length; " @@ -2900,7 +2900,7 @@ def setdiag(self, values, k=0, *, mask=None, accum=None, **opts): ) else: if mask.complement: - mask = mask.new().S + mask = mask.new(**opts).S M = mask.parent.diag() if M.shape != self.shape: M.resize(self._nrows, self._ncols) From 0f62bbdbb48384bb740b6e657d4fd7422aa18e3b Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 15 Sep 2023 10:05:42 +0200 Subject: [PATCH 08/10] Better handle setdiag with dimensions of size 0 --- graphblas/core/matrix.py | 14 ++++++++------ graphblas/tests/test_matrix.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 898765e7a..aed98f57d 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2828,17 +2828,19 @@ def setdiag(self, values, k=0, *, mask=None, accum=None, **opts): raise TypeError(f"k must be an integer; got bad type: {type(k)}") k = K if k < 0: - if (size := min(self._nrows + k, self._ncols)) <= 0: + if (size := min(self._nrows + k, self._ncols)) <= 0 and k <= -self._nrows: raise IndexError( f"k={k} is too small; the k'th diagonal is out of range. " - f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " - f"{-self._nrows} < k < {self.ncols}" + f"Valid k for Matrix with shape {self._nrows}x{self._ncols}: " + f"{-self._nrows} {'<' if self._nrows else '<='} k " + f"{'<' if self._ncols else '<='} {self._ncols}" ) - elif (size := min(self._ncols - k, self._nrows)) <= 0: + elif (size := min(self._ncols - k, self._nrows)) <= 0 and k > 0 and k >= self._ncols: raise IndexError( f"k={k} is too large; the k'th diagonal is out of range. " - f"Valid k for Matrix with shape {self.nrows}x{self.ncols}: " - f"{-self._nrows} < k < {self.ncols}" + f"Valid k for Matrix with shape {self._nrows}x{self._ncols}: " + f"{-self._nrows} {'<' if self._nrows else '<='} k " + f"{'<' if self._ncols else '<='} {self._ncols}" ) # Convert `values` to Vector if necessary (i.e., it's scalar or array) diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index a4d2ccd5c..e08f96b32 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -4439,6 +4439,37 @@ def test_setdiag(): with pytest.raises(DimensionMismatch, match="Dimensions not compatible"): A.setdiag([10], 1) + # Special care for dimensions of length 0 + A = Matrix(int, 0, 2, name="A") + A.setdiag(0, 0) + A.setdiag(0, 1) + A.setdiag([], 0) + A.setdiag([], 1) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(0, -1) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag([], -1) + A = Matrix(int, 2, 0, name="A") + A.setdiag(0, 0) + A.setdiag(0, -1) + A.setdiag([], 0) + A.setdiag([], -1) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(0, 1) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag([], 1) + A = Matrix(int, 0, 0, name="A") + A.setdiag(0, 0) + A.setdiag([], 0) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(0, 1) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag([], 1) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag(0, -1) + with pytest.raises(IndexError, match="diagonal is out of range"): + A.setdiag([], -1) + A = Matrix(int, 2, 2, name="A") expected = Matrix(int, 2, 2, name="expected") v = Vector(int, 2, name="v") From 7c2f0ae89474ed3267133a82a42eb0af739a3a7a Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 22 Sep 2023 16:10:51 +0100 Subject: [PATCH 09/10] Support numpy 1.26.0 --- .github/workflows/test_and_build.yml | 11 ++++++----- .pre-commit-config.yaml | 8 ++++---- graphblas/tests/test_ssjit.py | 5 +++++ scripts/check_versions.sh | 12 ++++++------ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 2f48048de..412709e02 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -169,17 +169,17 @@ jobs: sparsever=$(python -c 'import random ; print(random.choice(["=0.13", "=0.14", ""]))') fmmver=$(python -c 'import random ; print(random.choice(["=1.4", "=1.5", "=1.6", "=1.7", ""]))') if [[ ${{ startsWith(steps.pyver.outputs.selected, '3.9') }} == true ]]; then - npver=$(python -c 'import random ; print(random.choice(["=1.21", "=1.22", "=1.23", "=1.24", "=1.25", ""]))') + npver=$(python -c 'import random ; print(random.choice(["=1.21", "=1.22", "=1.23", "=1.24", "=1.25", "=1.26", ""]))') spver=$(python -c 'import random ; print(random.choice(["=1.8", "=1.9", "=1.10", "=1.11", ""]))') pdver=$(python -c 'import random ; print(random.choice(["=1.2", "=1.3", "=1.4", "=1.5", "=2.0", "=2.1", ""]))') akver=$(python -c 'import random ; print(random.choice(["=1.9", "=1.10", "=2.0", "=2.1", "=2.2", "=2.3", "=2.4", ""]))') elif [[ ${{ startsWith(steps.pyver.outputs.selected, '3.10') }} == true ]]; then - npver=$(python -c 'import random ; print(random.choice(["=1.21", "=1.22", "=1.23", "=1.24", "=1.25", ""]))') + npver=$(python -c 'import random ; print(random.choice(["=1.21", "=1.22", "=1.23", "=1.24", "=1.25", "=1.26", ""]))') spver=$(python -c 'import random ; print(random.choice(["=1.8", "=1.9", "=1.10", "=1.11", ""]))') pdver=$(python -c 'import random ; print(random.choice(["=1.3", "=1.4", "=1.5", "=2.0", "=2.1", ""]))') akver=$(python -c 'import random ; print(random.choice(["=1.9", "=1.10", "=2.0", "=2.1", "=2.2", "=2.3", "=2.4", ""]))') else # Python 3.11 - npver=$(python -c 'import random ; print(random.choice(["=1.23", "=1.24", "=1.25", ""]))') + npver=$(python -c 'import random ; print(random.choice(["=1.23", "=1.24", "=1.25", "=1.26", ""]))') spver=$(python -c 'import random ; print(random.choice(["=1.9", "=1.10", "=1.11", ""]))') pdver=$(python -c 'import random ; print(random.choice(["=1.5", "=2.0", "=2.1", ""]))') akver=$(python -c 'import random ; print(random.choice(["=1.10", "=2.0", "=2.1", "=2.2", "=2.3", "=2.4", ""]))') @@ -206,7 +206,7 @@ jobs: else psgver="" fi - if [[ ${npver} == "=1.25" ]] ; then + if [[ ${npver} == "=1.25" || ${npver} == "=1.26"]] ; then numbaver="" if [[ ${spver} == "=1.8" ]] ; then spver=$(python -c 'import random ; print(random.choice(["=1.9", "=1.10", "=1.11", ""]))') @@ -243,7 +243,8 @@ jobs: pdver="" yamlver="" fi - elif [[ ${npver} == "=1.25" ]] ; then + elif [[ ${npver} == "=1.25" || ${npver} == "=1.26" ]] ; then + # Don't install numba for unsupported versions of numpy numba="" numbaver=NA sparse="" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43ebe953c..a945fe49a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: - id: isort # Let's keep `pyupgrade` even though `ruff --fix` probably does most of it - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.12.0 hooks: - id: pyupgrade args: [--py39-plus] @@ -66,7 +66,7 @@ repos: - id: black - id: black-jupyter - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.289 + rev: v0.0.290 hooks: - id: ruff args: [--fix-only, --show-fixes] @@ -79,7 +79,7 @@ repos: additional_dependencies: &flake8_dependencies # These versions need updated manually - flake8==6.1.0 - - flake8-bugbear==23.7.10 + - flake8-bugbear==23.9.16 - flake8-simplify==0.20.0 - repo: https://github.com/asottile/yesqa rev: v1.5.0 @@ -94,7 +94,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.289 + rev: v0.0.290 hooks: - id: ruff - repo: https://github.com/sphinx-contrib/sphinx-lint diff --git a/graphblas/tests/test_ssjit.py b/graphblas/tests/test_ssjit.py index 57cb2bbba..bd05cf2db 100644 --- a/graphblas/tests/test_ssjit.py +++ b/graphblas/tests/test_ssjit.py @@ -1,4 +1,5 @@ import os +import pathlib import sys import numpy as np @@ -82,6 +83,10 @@ def _setup_jit(): gb.ss.config["jit_c_libraries"] = "" gb.ss.config["jit_c_cmake_libs"] = "" + if not pathlib.Path(gb.ss.config["jit_c_compiler_name"]).exists(): + # Can't use the JIT if we don't have a compiler! + gb.ss.config["jit_c_control"] = "off" + @pytest.fixture def v(): diff --git a/scripts/check_versions.sh b/scripts/check_versions.sh index b5f391619..a76fee1d2 100755 --- a/scripts/check_versions.sh +++ b/scripts/check_versions.sh @@ -3,15 +3,15 @@ # Use, adjust, copy/paste, etc. as necessary to answer your questions. # This may be helpful when updating dependency versions in CI. # Tip: add `--json` for more information. -conda search 'numpy[channel=conda-forge]>=1.25.2' -conda search 'pandas[channel=conda-forge]>=2.1.0' +conda search 'flake8-bugbear[channel=conda-forge]>=23.9.16' +conda search 'flake8-simplify[channel=conda-forge]>=0.20.0' +conda search 'numpy[channel=conda-forge]>=1.26.0' +conda search 'pandas[channel=conda-forge]>=2.1.1' conda search 'scipy[channel=conda-forge]>=1.11.2' conda search 'networkx[channel=conda-forge]>=3.1' -conda search 'awkward[channel=conda-forge]>=2.4.2' +conda search 'awkward[channel=conda-forge]>=2.4.3' conda search 'sparse[channel=conda-forge]>=0.14.0' -conda search 'fast_matrix_market[channel=conda-forge]>=1.7.2' +conda search 'fast_matrix_market[channel=conda-forge]>=1.7.3' conda search 'numba[channel=conda-forge]>=0.57.1' conda search 'pyyaml[channel=conda-forge]>=6.0.1' -conda search 'flake8-bugbear[channel=conda-forge]>=23.7.10' -conda search 'flake8-simplify[channel=conda-forge]>=0.20.0' # conda search 'python[channel=conda-forge]>=3.9 *pypy*' From 9e51b879963f72ee9a5689fef8d9fc8e905e2a39 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 22 Sep 2023 16:16:22 +0100 Subject: [PATCH 10/10] oops --- .github/workflows/test_and_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 412709e02..5f1ab7dde 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -206,7 +206,7 @@ jobs: else psgver="" fi - if [[ ${npver} == "=1.25" || ${npver} == "=1.26"]] ; then + if [[ ${npver} == "=1.25" || ${npver} == "=1.26" ]] ; then numbaver="" if [[ ${spver} == "=1.8" ]] ; then spver=$(python -c 'import random ; print(random.choice(["=1.9", "=1.10", "=1.11", ""]))')