Skip to content
Open
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 graphblas/core/operator/agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,4 +776,6 @@ def _first_last_index(agg, updater, expr, opts, *, in_composite, semiring):
agg.Aggregator = Aggregator
agg.TypedAggregator = TypedAggregator

from .utils import get_typed_op # noqa: E402 isort:skip
from .utils import _register_aggregator_types, get_typed_op # noqa: E402 isort:skip

_register_aggregator_types(Aggregator, TypedAggregator)
306 changes: 296 additions & 10 deletions graphblas/core/operator/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@
raise


# ``agg.py`` imports ``get_typed_op`` from this module, so the two form an import
# cycle and we can't import the Aggregator classes at module load. ``agg.py``
# registers them here when it loads, which ``core.operator.__init__`` does
# eagerly at import. Before registration no Aggregator instance can exist, so
# ``get_typed_op`` skips the check entirely.
_Aggregator = None
_TypedAggregator = None


def _register_aggregator_types(aggregator, typed_aggregator):
global _Aggregator, _TypedAggregator
_Aggregator = aggregator
_TypedAggregator = typed_aggregator


def get_typed_op(op, dtype, dtype2=None, *, is_left_scalar=False, is_right_scalar=False, kind=None):
if isinstance(op, OpBase):
# UDTs always get compiled
Expand Down Expand Up @@ -93,15 +108,14 @@ def get_typed_op(op, dtype, dtype2=None, *, is_left_scalar=False, is_right_scala
if isinstance(op, TypedOpBase):
return op

from .agg import Aggregator, TypedAggregator

if isinstance(op, Aggregator):
# agg._any_dtype basically serves the same purpose as op._custom_dtype
if op._any_dtype is not None and op._any_dtype is not True:
return op[op._any_dtype]
return op[dtype]
if isinstance(op, TypedAggregator):
return op
if _Aggregator is not None:
if isinstance(op, _Aggregator):
# agg._any_dtype basically serves the same purpose as op._custom_dtype
if op._any_dtype is not None and op._any_dtype is not True:
return op[op._any_dtype]
return op[dtype]
if isinstance(op, _TypedAggregator):
return op
if isinstance(op, str):
if kind == "unary":
op = unary_from_string(op)
Expand Down Expand Up @@ -280,6 +294,52 @@ def get_semiring(monoid, binaryop, name=None):
return rv


def _resolve_index_expr(ns, modname, expr, callname, opname):
"""Turn an infix comparison expression into an indexunary/select op call.

Shared by the ``value``/``row``/``column`` helpers in the ``select`` and
``indexunary`` namespaces (and ``select.index``). ``select.valuegt`` is a
SelectOp that defaults to ``.select``, while ``indexunary.valuegt`` is an
IndexUnaryOp that defaults to ``.apply``, so the same expression resolves to
a select or an apply depending on which namespace's ops ``ns`` holds. ``ns``
is the namespace module's ``globals()`` dict and ``modname`` is its short
name (used in the error messages).
"""
from ..base import BaseExpression

if not isinstance(expr, BaseExpression):
raise TypeError(
f"Expected ScalarExpression, VectorExpression, or MatrixExpression; "
f"found {type(expr)}\nTypical usage: {modname}.{callname}(x <= 5)"
)
tensor = expr.args[0]
thunk = expr.args[1]
method = f"{opname}{expr.op.name}"
if method not in ns:
# TODO: remove this once rowlt/rowge/collt/colge exist
# Convert thunk to Python int to avoid possible subtraction with uints
thunk = thunk.value
# Attempt to convert < into <= (rowlt is not part of official spec, but rowle is)
if expr.op.name == "lt":
method = f"{opname}le"
thunk -= 1
# Attempt to convert >= into > (rowge is not part of official spec, but rowgt is)
elif expr.op.name == "ge":
method = f"{opname}gt"
thunk -= 1
if method not in ns: # pragma: no cover (sanity)
raise ValueError(f"Unknown or unregistered {modname} method: {method}")
if expr._is_scalar:
# Handle ScalarExpressions that change their arguments to Vector
if tensor._parent is not None: # e.g., suitesparse
tensor = tensor._parent
thunk = thunk._parent
else: # e.g., suitesparse-vanilla
tensor = tensor[0].new()
thunk = thunk[0].new()
return ns[method](tensor, thunk)


unary.register_new = UnaryOp.register_new
unary.register_anonymous = UnaryOp.register_anonymous
indexbinary.register_new = IndexBinaryOp.register_new
Expand Down Expand Up @@ -417,28 +477,196 @@ def _from_string(string, module, mapping, example):


def unary_from_string(string):
"""Look up a UnaryOp by name or symbol, optionally typed with ``"[dtype]"``.

Backs ``gb.unary.from_string`` and the string coercion used wherever a
UnaryOp is accepted, such as ``v.apply("abs")``.

Parameters
----------
string : str
A name in the ``gb.unary`` namespace (``"abs"``, or a dotted path such
as ``"numpy.negative"``) or a symbolic shorthand (``"-"`` for ``ainv``,
``"~"`` for ``lnot``). Append ``"[dtype]"`` to type the operator, as in
``"abs[int]"``.

Returns
-------
UnaryOp

See Also
--------
unary.register_new
op.from_string

Examples
--------
>>> gb.unary.from_string("abs") is gb.unary.abs
True
>>> gb.unary.from_string("abs[int]") is gb.unary.abs[int]
True

"""
return _from_string(string, unary, _str_to_unary, "abs[int]")


def indexunary_from_string(string):
"""Look up an IndexUnaryOp by name, optionally typed with ``"[dtype]"``.

Parameters
----------
string : str
A name in the ``gb.indexunary`` namespace, such as ``"rowindex"``,
``"diag"``, or ``"tril"``. Append ``"[dtype]"`` to type the operator,
as in ``"rowindex[int]"``.

Returns
-------
IndexUnaryOp

See Also
--------
indexunary.register_new
select.from_string

Examples
--------
>>> gb.indexunary.from_string("rowindex") is gb.indexunary.rowindex
True

"""
# "select" is a variant of IndexUnary, so the string abbreviations in
# _str_to_select are appropriate to reuse here
return _from_string(string, indexunary, _str_to_select, "row_index")
return _from_string(string, indexunary, _str_to_select, "rowindex")


def select_from_string(string):
"""Look up a SelectOp by name or comparison symbol.

Parameters
----------
string : str
A name in the ``gb.select`` namespace (``"tril"``, ``"triu"``,
``"offdiag"``, ``"valuegt"``, ...) or a comparison shorthand such as
``">="`` (``valuege``), ``"=="`` (``valueeq``), or ``"row>"``
(``rowgt``).

Returns
-------
SelectOp

See Also
--------
select.register_new
indexunary.from_string

Examples
--------
>>> gb.select.from_string("tril") is gb.select.tril
True
>>> gb.select.from_string(">=") is gb.select.valuege
True

"""
return _from_string(string, select, _str_to_select, "tril")


def binary_from_string(string):
"""Look up a BinaryOp by name or symbol, optionally typed with ``"[dtype]"``.

Backs ``gb.binary.from_string`` and the string coercion used wherever a
BinaryOp is accepted, such as ``A.ewise_mult(B, "+")``.

Parameters
----------
string : str
A name in the ``gb.binary`` namespace (``"plus"``, or a dotted path such
as ``"numpy.mod"``) or an arithmetic/comparison shorthand such as ``"+"``
(``plus``), ``"*"`` (``times``), or ``">="`` (``ge``). Append
``"[dtype]"`` to type the operator, as in ``"plus[int]"``.

Returns
-------
BinaryOp

See Also
--------
binary.register_new
op.from_string

Examples
--------
>>> gb.binary.from_string("+") is gb.binary.plus
True
>>> gb.binary.from_string("minus[int]") is gb.binary.minus[int]
True

"""
return _from_string(string, binary, _str_to_binary, "+[int]")


def monoid_from_string(string):
"""Look up a Monoid by name or symbol, optionally typed with ``"[dtype]"``.

Parameters
----------
string : str
A name in the ``gb.monoid`` namespace (``"plus"``, ``"times"``, ...) or a
symbolic shorthand such as ``"+"`` (``plus``), ``"*"`` (``times``), or
``"|"`` (``lor``). Append ``"[dtype]"`` to type the monoid, as in
``"plus[float]"``.

Returns
-------
Monoid

See Also
--------
monoid.register_new
semiring.from_string

Examples
--------
>>> gb.monoid.from_string("+[float]") is gb.monoid.plus[float]
True

"""
return _from_string(string, monoid, _str_to_monoid, "+[int]")


def semiring_from_string(string):
"""Look up a Semiring by name, optionally typed with ``"[dtype]"``.

A semiring pairs a monoid with a binaryop. Name it either as the combined
namespace attribute (``"plus_times"``) or in ``"monoid.binaryop"`` form using
the monoid and binaryop shorthands (``"min.+"``); the two parts must be
separated by exactly one period.

Parameters
----------
string : str
The semiring name, such as ``"plus_times"`` or ``"min_plus"``, or the
``"monoid.binaryop"`` form ``"min.+"``. Append ``"[dtype]"`` to type the
semiring, as in ``"min.+[int]"``.

Returns
-------
Semiring

See Also
--------
semiring.register_new
semiring.get_semiring
op.from_string

Examples
--------
>>> gb.semiring.from_string("min.+") is gb.semiring.min_plus
True
>>> gb.semiring.from_string("min_plus") is gb.semiring.min_plus
True

"""
split = string.split(".")
if len(split) == 1:
try:
Expand All @@ -457,6 +685,38 @@ def semiring_from_string(string):


def op_from_string(string):
"""Look up an operator of any kind by string.

Each operator type is tried in turn (unary, binary, monoid, semiring,
indexunary, select, then aggregator) and the first match is returned, so an
unqualified name resolves to whichever kind defines it first. Use a
type-specific ``from_string`` (e.g. ``gb.binary.from_string``) when the kind
is known and matters.

Parameters
----------
string : str
An operator name or symbol accepted by any of the type-specific
``from_string`` functions, optionally typed with ``"[dtype]"``.

Returns
-------
UnaryOp, BinaryOp, Monoid, Semiring, IndexUnaryOp, SelectOp, or Aggregator

See Also
--------
unary.from_string
binary.from_string
semiring.from_string

Examples
--------
>>> gb.op.from_string("+") is gb.binary.plus
True
>>> gb.op.from_string("min.plus") is gb.semiring.min_plus
True

"""
for func in [
# Note: order matters here
unary_from_string,
Expand Down Expand Up @@ -491,6 +751,32 @@ def op_from_string(string):


def aggregator_from_string(string):
"""Look up an Aggregator by name or symbol, optionally typed with ``"[dtype]"``.

Parameters
----------
string : str
A name in the ``gb.agg`` namespace (``"sum"``, ``"count"``, ``"any"``,
...) or a symbolic shorthand such as ``"+"`` (``sum``), ``"*"``
(``prod``), ``"&"`` (``all``), or ``"|"`` (``any``). Append ``"[dtype]"``
to type the aggregator, as in ``"sum[int]"``.

Returns
-------
Aggregator

See Also
--------
op.from_string

Examples
--------
>>> gb.agg.from_string("sum[int]") is gb.agg.sum[int]
True
>>> gb.agg.from_string("|") is gb.agg.any
True

"""
return _from_string(string, agg, _str_to_agg, "sum[int]")


Expand Down
Loading
Loading