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
4 changes: 3 additions & 1 deletion docs/user_guide/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ with an INT8 vector ``v``, ``v + 1`` is INT8 and ``v * 0.5`` is FP64, while
``v + 300`` raises ``OverflowError`` rather than wrapping, and ``v * 2`` wraps
in INT8 as it does in numpy. An FP32 vector times ``0.5`` is FP32, plus ``1j``
is FC32, and plus ``1e300`` is infinity, with a ``RuntimeWarning`` as in numpy. numpy scalars, 0-d arrays and ``Scalar`` objects keep their own
type: ``v + np.int64(1)`` is INT64. python-graphblas applies this rule itself,
type: ``v + np.int64(1)`` is INT64, and so is ``np.int64(1) + v``, since numpy's
operators leave an operation with a python-graphblas object to python-graphblas.
python-graphblas applies this rule itself,
so it does not depend on the installed version of numpy. User-defined types
follow the same rule, field by field (see :doc:`udt`).

Expand Down
7 changes: 7 additions & 0 deletions graphblas/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ class BaseType:
__slots__ = "gb_obj", "dtype", "name", "__weakref__"
# Flag for operations which depend on scalar vs vector/matrix
_is_scalar = False
# numpy's operators return NotImplemented for an operand of higher priority than theirs
# (ndarray's is 0, np.matrix's 10), so with a numpy scalar or array on the left Python calls
# our reflected method, and `np.int16(2) - s` is the expression `s - np.int16(2)` would be,
# not numpy's value of `s.__array__()`. Ufuncs such as `np.sin(s)` still use __array__.
# Expressions, TransposedMatrix and the index expressions set it too.
__array_priority__ = 1000

def __call__(
self,
Expand Down Expand Up @@ -577,6 +583,7 @@ class BaseExpression:
)
output_type = None
_is_scalar = False
__array_priority__ = BaseType.__array_priority__ # numpy's operators defer to ours

def __init__(
self,
Expand Down
6 changes: 6 additions & 0 deletions graphblas/core/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ def __bool__(self):
class AmbiguousAssignOrExtract:
__slots__ = "parent", "resolved_indexes", "_value", "__weakref__"
_is_scalar = False
__array_priority__ = 1000 # numpy's operators defer to ours; see BaseType

def __init__(self, parent, resolved_indexes):
self.parent = parent
Expand Down Expand Up @@ -484,6 +485,7 @@ def __bool__(self):
class InfixExprBase:
__slots__ = "left", "right", "_expr", "__weakref__"
_is_scalar = False
__array_priority__ = 1000 # numpy's operators defer to ours; see BaseType

def __init__(self, left, right):
self.left = left
Expand Down Expand Up @@ -511,6 +513,10 @@ def _to_expr(self):
self._expr = getattr(self.left, self.method_name)(self.right)
return self._expr

def _call_left(self, method_name, *args, **kwargs):
"""Call the left operand's ``method_name`` with the right operand, as ``op(x & y)`` does."""
return getattr(self.left, method_name)(self.right, *args, **kwargs)

def _get_value(self, attr=None, default=None):
expr = self._to_expr()
return expr._get_value(attr=attr, default=default)
Expand Down
41 changes: 28 additions & 13 deletions graphblas/core/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .. import backend, config, monoid, unary
from ..dtypes import BOOL
from ..exceptions import OutOfMemory
from .infix import _is_literal
from .matrix import Matrix, TransposedMatrix
from .vector import Vector

Expand Down Expand Up @@ -717,11 +718,7 @@ def _format_infix_expression(expr, header, expr_name):
)

computed = ""
if config.get("autocompute") and (
expr.method_name not in {"ewise_add", "ewise_mult"}
or expr.left.dtype == BOOL
and expr.right.dtype == BOOL
):
if config.get("autocompute") and _computes_itself(expr):
computed = get_expr_result(expr, html=True)
if "__EXPR__" in computed:
return computed.replace("<tt>__EXPR__</tt>", topline)
Expand All @@ -740,17 +737,39 @@ def _format_infix_expression(expr, header, expr_name):
)


def _operand_dtype(operand):
"""The dtype to show for an operand of a Scalar infix expression.

A literal, as in ``s & 1``, has no dtype until an op types it, so its own
type is shown.
"""
if _is_literal(operand):
return type(operand).__name__
return operand.dtype


def _computes_itself(expr):
"""Whether infix ``expr`` computes without an op, as ``x @ y`` and BOOL ``x & y`` do."""
if expr.method_name not in {"ewise_add", "ewise_mult"}:
return True
# A literal beside a Scalar, as in `s & True`, is BOOL beside BOOL if it is a bool
return all(
type(x) in {bool, np.bool_} if _is_literal(x) else x.dtype == BOOL
for x in (expr.left, expr.right)
)


def format_scalar_infix_expression(expr):
expr_repr = expr._format_expr()
name = f"gb.{type(expr).__name__}"
header = create_header(
expr_repr,
["left_dtype", "right_dtype"],
[expr.left.dtype, expr.right.dtype],
[_operand_dtype(expr.left), _operand_dtype(expr.right)],
name=name,
quote=False,
)
arg_string = get_result_string(expr)
arg_string = get_infix_result_string(expr)
return (
f"{header}\n\n"
f"{arg_string}"
Expand All @@ -764,17 +783,13 @@ def format_scalar_infix_expression_html(expr):
header = create_header_html(
expr_html,
["left_dtype", "right_dtype"],
[expr.left.dtype, expr.right.dtype],
[_operand_dtype(expr.left), _operand_dtype(expr.right)],
)
return _format_infix_expression(expr, header, expr_html)


def get_infix_result_string(expr):
if (
expr.method_name not in {"ewise_add", "ewise_mult"}
or expr.left.dtype == BOOL
and expr.right.dtype == BOOL
):
if _computes_itself(expr):
arg_string = get_result_string(expr)
else:
arg_string = ""
Expand Down
96 changes: 83 additions & 13 deletions graphblas/core/infix.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import numpy as np

from .. import backend, binary
from ..dtypes import BOOL
from ..exceptions import DimensionMismatch
Expand All @@ -6,25 +8,37 @@
from . import automethods, recorder, utils
from .base import _expect_op, _expect_type
from .expr import InfixExprBase
from .literals import _LITERAL_KINDS, _literal_operand
from .mask import Mask
from .matrix import Matrix, MatrixExpression, TransposedMatrix
from .scalar import Scalar, ScalarExpression
from .scalar import Scalar, ScalarExpression, _literal_scalar
from .utils import output_type, wrapdoc
from .vector import Vector, VectorExpression

InfixExprBase._expect_op = _expect_op
InfixExprBase._expect_type = _expect_type


def _typed_operands(self, op):
"""Return the operands of ``self``, with a literal beside a Scalar typed as ``op``'s operand."""
left, right = self.left, self.right
if _is_literal(left):
left = _literal_scalar(right, left, op, within=self.method_name, argname="left")
elif _is_literal(right):
right = _literal_scalar(left, right, op, within=self.method_name, argname="right")
return left, right


def _ewise_add_to_expr(self):
if self._expr is not None:
return self._expr
if self.left.dtype == BOOL and self.right.dtype == BOOL:
self._expr = self.left.ewise_add(self.right, lor)
left, right = _typed_operands(self, lor)
if left.dtype == BOOL and right.dtype == BOOL:
self._expr = left.ewise_add(right, lor)
return self._expr
raise TypeError(
"Bad dtypes for `x | y`! Automatic computation of `x | y` infix expressions is only valid "
f"for BOOL dtypes. The argument dtypes are {self.left.dtype} and {self.right.dtype}.\n\n"
f"for BOOL dtypes. The argument dtypes are {left.dtype} and {right.dtype}.\n\n"
"When auto-computed for boolean dtypes, `x | y` performs ewise_add (union) using LOR.\n\n"
"Typical usage is to create an ewise_add expression such as `monoid.plus(x | y)`."
)
Expand All @@ -33,18 +47,31 @@ def _ewise_add_to_expr(self):
def _ewise_mult_to_expr(self):
if self._expr is not None:
return self._expr
if self.left.dtype == BOOL and self.right.dtype == BOOL:
self._expr = self.left.ewise_mult(self.right, land)
left, right = _typed_operands(self, land)
if left.dtype == BOOL and right.dtype == BOOL:
self._expr = left.ewise_mult(right, land)
return self._expr
raise TypeError(
"Bad dtypes for `x & y`! Automatic computation of `x & y` infix expressions is only valid "
f"for BOOL dtypes. The argument dtypes are {self.left.dtype} and {self.right.dtype}.\n\n"
f"for BOOL dtypes. The argument dtypes are {left.dtype} and {right.dtype}.\n\n"
"When auto-computed for boolean dtypes, `x & y` performs ewise_mult (intersection) using "
"LAND.\n\n"
"Typical usage is to create an ewise_mult expression such as `monoid.times(x & y)`."
)


def _is_literal(operand):
"""Whether an operand of an infix expression is a literal, which only a Scalar takes."""
return output_type(operand) not in {Scalar, Vector, Matrix, TransposedMatrix}


def _operand_name(operand, *, html=False):
if _is_literal(operand):
# As in `s & 1`; it shows its value, as an unnamed Scalar does
return repr(operand)
return operand._name_html if html else operand.name


class ScalarInfixExpr(InfixExprBase):
__slots__ = ()
ndim = 0
Expand All @@ -70,6 +97,21 @@ def dup(self, dtype=None, *, clear=False, is_cscalar=False, name=None, **opts):
def is_grbscalar(self):
return not self.is_cscalar

def _format_expr(self):
return f"{_operand_name(self.left)} {self._infix} {_operand_name(self.right)}"

def _format_expr_html(self):
left = _operand_name(self.left, html=True)
right = _operand_name(self.right, html=True)
return f"{left} {self._infix} {right}"

def _call_left(self, method_name, *args, **kwargs):
if _is_literal(self.left):
# A literal stays on the left: `binary.minus(1 & s)` is `1 - s`. Scalar's method
# types it beside `s` once the op is known, as it types a literal on the right.
return getattr(Scalar, method_name)(self.left, self.right, *args, **kwargs)
return super()._call_left(method_name, *args, **kwargs)

__hash__ = None

# Begin auto-generated code: Scalar
Expand Down Expand Up @@ -485,7 +527,7 @@ def __init__(self, left, right, *, nrows, ncols):
utils._output_types[MatrixMatMulExpr] = Matrix


def _dummy(obj, obj_type):
def _dummy(obj):
with recorder.skip_record:
return output_type(obj)(BOOL, *obj.shape, name="")

Expand All @@ -498,6 +540,33 @@ def _mismatched(left, right, method, op):
raise DimensionMismatch # pragma: no cover


def _check_literal(scalar, value, *, within, argname):
"""Raise TypeError now if ``value`` cannot be a literal beside ``scalar``, as ``+`` does.

How a literal is typed depends on the op (``literals._literal_operand``), which
comes later, so here it is typed as ``==`` types it, the op that takes the most
literals, and dropped. A Python number is always a literal, so it is not checked.
Nor is a literal beside an infix expression, which has no dtype until an op is
applied: ``(s & t) & object()`` raises only then.
"""
if type(value) in _LITERAL_KINDS or isinstance(scalar, ScalarInfixExpr):
return
value, dtype = _literal_operand(scalar.dtype, value, binary.eq)
try:
# A float too large for a float32 leaf warns once, when the op types the literal
with np.errstate(over="ignore"):
Scalar.from_value(value, dtype, is_cscalar=False, name="")
except TypeError:
_expect_type(
scalar,
value,
Scalar,
within=within,
argname=argname,
extra_message="Literal scalars also accepted.",
)


def _ewise_infix_expr(left, right, *, method, within):
left_type = output_type(left)
right_type = output_type(right)
Expand Down Expand Up @@ -530,14 +599,15 @@ def _ewise_infix_expr(left, right, *, method, within):
left._expect_type(right, tuple(types), within=within, argname="right")
if right_type in types:
right._expect_type(left, tuple(types), within=within, argname="left")
if left_type is Scalar:
if left_type is Scalar or right_type is Scalar:
# A literal operand stays a literal, on its own side, until an op types it
if left_type is not Scalar:
_check_literal(right, left, within=within, argname="left")
elif right_type is not Scalar:
_check_literal(left, right, within=within, argname="right")
if method == "ewise_mult":
return ScalarEwiseMultExpr(left, right)
return ScalarEwiseAddExpr(left, right)
if right_type is Scalar:
if method == "ewise_mult":
return ScalarEwiseMultExpr(right, left)
return ScalarEwiseAddExpr(right, left)
raise TypeError( # pragma: no cover (sanity)
f"Bad types for ewise infix: {type(left).__name__}, {type(right).__name__}"
)
Expand Down
20 changes: 9 additions & 11 deletions graphblas/core/infixmethods.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from ..dtypes import BOOL
from .infix import MatrixInfixExpr, ScalarInfixExpr, VectorInfixExpr
from .matrix import Matrix, MatrixExpression, MatrixIndexExpr, TransposedMatrix
from .scalar import Scalar, ScalarExpression, ScalarIndexExpr
from .scalar import Scalar, ScalarExpression, ScalarIndexExpr, _literal_scalar
from .udt import _ewise_add_casts_udt, _plus_zero
from .utils import output_type
from .vector import Vector, VectorExpression, VectorIndexExpr
Expand Down Expand Up @@ -32,18 +32,16 @@ def call_op(self, other, op, *, outer=False, union=False):
return op(self, other)
if type2 in types:
return op(self, other)
# Scalars
# Scalars. A literal on the left stays there: `1 - s` is minus(1, s), typed as
# `binary.minus(1 & s)` is, since rminus may not exist for s's dtype (a UDT).
if type1 is not Scalar:
method = "ewise_add" if outer else "ewise_union" if union else "ewise_mult"
self = _literal_scalar(other, self, op, within=method, argname="left")
if outer:
if type1 is Scalar:
return self.ewise_add(other, op)
return other.ewise_add(self, op.commutes_to)
return self.ewise_add(other, op)
if union:
if type1 is Scalar:
return self.ewise_union(other, op, False, False)
return other.ewise_union(self, op.commutes_to, False, False)
if type1 is Scalar:
return self.ewise_mult(other, op)
return other.ewise_mult(self, op.commutes_to)
return self.ewise_union(other, op, False, False)
return self.ewise_mult(other, op)


def __divmod__(self, other):
Expand Down
1 change: 1 addition & 0 deletions graphblas/core/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -3893,6 +3893,7 @@ class TransposedMatrix:
ndim = 2
_is_scalar = False
_is_transposed = True
__array_priority__ = BaseType.__array_priority__ # numpy's operators defer to ours
__networkx_backend__ = "graphblas"
__networkx_plugin__ = "graphblas"

Expand Down
4 changes: 1 addition & 3 deletions graphblas/core/operator/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,7 @@ def _call_op(op, left, right=None, thunk=None, **kwargs):
if right is None and thunk is None:
if isinstance(left, InfixExprBase):
# op(A & B), op(A | B), op(A @ B)
return getattr(left.left, f"_{left.method_name}")(
left.right, op, is_infix=True, **kwargs
)
return left._call_left(f"_{left.method_name}", op, is_infix=True, **kwargs)
if find_opclass(op)[1] == "Semiring":
raise TypeError(
f"Bad type when calling {op!r}. Got type: {type(left)}.\n"
Expand Down
4 changes: 1 addition & 3 deletions graphblas/core/operator/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,7 @@ def __call__(self, left, right=None, *, left_default=None, right_default=None):
f">>> {self}(x | y, left_default=0, right_default=0)\n\nwhere x and y "
"are Vectors or Matrices, and left_default and right_default are scalars."
)
return left.left._ewise_union(
left.right, self, left_default, right_default, is_infix=True
)
return left._call_left("_ewise_union", self, left_default, right_default, is_infix=True)
return _call_op(self, left, right)

@property
Expand Down
Loading
Loading