Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions graphblas/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from . import _automethods, backend, binary, ffi, lib, monoid, select, semiring, utils
from ._ss.matrix import ss
from .base import BaseExpression, BaseType, call
from .base import BaseExpression, BaseType, _check_mask, call
from .dtypes import _INDEX, FP64, lookup_dtype, unify
from .exceptions import DimensionMismatch, NoValue, check_status
from .expr import AmbiguousAssignOrExtract, IndexerResolver, Updater
from .mask import StructuralMask, ValueMask
from .operator import find_opclass, get_semiring, get_typed_op, op_from_string
from .mask import Mask, StructuralMask, ValueMask
from .operator import UNKNOWN_OPCLASS, find_opclass, get_semiring, get_typed_op, op_from_string
from .scalar import (
_MATERIALIZE,
Scalar,
Expand All @@ -28,7 +28,7 @@
values_to_numpy_buffer,
wrapdoc,
)
from .vector import Vector, VectorExpression, VectorIndexExpr
from .vector import Vector, VectorExpression, VectorIndexExpr, _select_mask

ffi_new = ffi.new

Expand Down Expand Up @@ -891,9 +891,31 @@ def select(self, op, thunk=None):
Compute SelectOp at each element of the calling Matrix, keeping
elements which return True.
"""
method_name = "select"
if isinstance(op, str):
op = select.from_string(op)
method_name = "select"
else:
op, opclass = find_opclass(op)
if opclass == UNKNOWN_OPCLASS:
# Optimization opportunity: we could be smarter and change e.g. `A.select(A == 7)`
# to `gb.select.value(A == 7)` or `A.select(gb.select.valueeq, 7)`.
# Should we allow e.g., `A.select(B.T)`?
mask = _check_mask(op)
if thunk is not None:
raise TypeError(
"thunk argument not None when calling select with mask or boolean object"
)
self._expect_type(mask.parent, (Matrix, Mask), within=method_name, argname="op")
return MatrixExpression(
"select",
None,
[self, mask, _select_mask, (self, mask)], # [*expr_args, func, args]
expr_repr="{0.name}.select({1.name})",
nrows=self.nrows,
ncols=self.ncols,
dtype=self.dtype,
)

if thunk is None:
thunk = False # most basic form of 0 when unifying dtypes
if type(thunk) is not Scalar:
Expand Down
12 changes: 12 additions & 0 deletions graphblas/tests/test_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,18 @@ def test_select(A):
assert w7.isequal(Aupper)
with pytest.raises(TypeError, match="thunk"):
A.select(select.valueeq, object())
# Select with boolean and masks
w8 = A.select((A == 3).new()).new()
assert w8.isequal(A3)
w9 = A.select(w8.S).new()
assert w9.isequal(A3)
w8[0, 1] = 0
w10 = A.select(w8.V).new()
assert w10.isequal(A3)
with pytest.raises(TypeError, match="thunk"):
A.select(w8.V, 777)
with pytest.raises(TypeError):
A.select(A[0, :].new().S)


@pytest.mark.slow
Expand Down
39 changes: 39 additions & 0 deletions graphblas/tests/test_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,45 @@ def test_select(v):
assert w6.isequal(result)
with pytest.raises(TypeError, match="thunk"):
v.select(select.valueeq, object())
# Select with boolean and masks
w7 = v.select((v == 1).new()).new()
assert w7.isequal(result)
w8 = v.select(w7.S).new()
assert w8.isequal(result)
w7[4] = 0
w9 = v.select(w7.V).new()
assert w9.isequal(result)
with pytest.raises(TypeError, match="thunk"):
v.select(w7.V, 777)
with pytest.raises(TypeError):
v.select(v.outer(v).new().S)
# Make sure we use `replace`
w9 << 1
w9 << v.select(w7.V)
assert w9.isequal(result)
# and with masks
w9 << 1
w9[1] = 0
w9(w9.V) << v.select(w7.V)
result2 = Vector.from_values([1, 3], [0, 1], size=7)
assert w9.isequal(result2)
w9 << 1
w9[1] = 0
w9(w9.V, replace=True) << v.select(w7.V)
del result2[1]
assert w9.isequal(result2)
w9 << 1
w9[1] = 0
w9(w9.V, binary.plus) << v.select(w7.V)
w8 << 1
w8[1] = 0
w8(w8.V, binary.plus) << v.dup(mask=w7.V)
assert w8.isequal(w9)
w9 << 1
w9(binary.plus) << v.select(w7.V)
w8 << 1
w8(binary.plus) << v.dup(mask=w7.V)
assert w8.isequal(w9)


@pytest.mark.slow
Expand Down
44 changes: 40 additions & 4 deletions graphblas/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from . import _automethods, backend, binary, ffi, lib, monoid, select, semiring, utils
from ._ss.vector import ss
from .base import BaseExpression, BaseType, call
from .base import BaseExpression, BaseType, _check_mask, call
from .dtypes import _INDEX, FP64, INT64, lookup_dtype, unify
from .exceptions import DimensionMismatch, NoValue, check_status
from .expr import AmbiguousAssignOrExtract, IndexerResolver, Updater
from .mask import StructuralMask, ValueMask
from .operator import find_opclass, get_semiring, get_typed_op, op_from_string
from .mask import Mask, StructuralMask, ValueMask
from .operator import UNKNOWN_OPCLASS, find_opclass, get_semiring, get_typed_op, op_from_string
from .scalar import (
_MATERIALIZE,
Scalar,
Expand Down Expand Up @@ -55,6 +55,22 @@ def _reposition(updater, indices, chunk):
updater[indices] = chunk


def _select_mask(updater, obj, mask):
if updater.kwargs.get("mask") is None:
orig_kwargs = updater.kwargs
try:
if updater.kwargs.get("accum") is None:
updater.kwargs = dict(orig_kwargs, mask=mask, replace=True)
else:
updater.kwargs = dict(orig_kwargs, mask=mask)
updater << obj
finally:
updater.kwargs = orig_kwargs
else:
# Can we do any better depending on accum, replace, and type of masks?
updater << obj.dup(mask=mask)


class Vector(BaseType):
"""
GraphBLAS Sparse Vector
Expand Down Expand Up @@ -807,9 +823,29 @@ def select(self, op, thunk=None):
Compute SelectOp at each element of the calling Vector, keeping
elements which return True.
"""
method_name = "select"
if isinstance(op, str):
op = select.from_string(op)
method_name = "select"
else:
op, opclass = find_opclass(op)
if opclass == UNKNOWN_OPCLASS:
# Optimization opportunity: we could be smarter and change e.g. `v.select(v == 7)`
# to `gb.select.value(v == 7)` or `v.select(gb.select.valueeq, 7)`.
mask = _check_mask(op)
if thunk is not None:
raise TypeError(
"thunk argument not None when calling select with mask or boolean object"
)
self._expect_type(mask.parent, (Vector, Mask), within=method_name, argname="op")
return VectorExpression(
"select",
None,
[self, mask, _select_mask, (self, mask)], # [*expr_args, func, args]
expr_repr="{0.name}.select({1.name})",
size=self.size,
dtype=self.dtype,
)

if thunk is None:
thunk = False # most basic form of 0 when unifying dtypes
if type(thunk) is not Scalar:
Expand Down