From ec482c8df778b8cdf37bb91b7793d8b8bf9273db Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 31 Jul 2026 14:08:10 -0500 Subject: [PATCH 1/8] Fix the README UDF example and add a Types API reference page The README UDF example imported unary but then used Vector without importing it, so the snippet as published raised NameError: name 'Vector' is not defined. Add Vector to the import; the copy in docs/user_guide/udf.rst was already correct. Also a new docs/api_reference/types.rst, wired into the reference toctree, surfacing DataType's JIT introspection properties (jit_c_name, jit_c_definition) and the dtypes register_new / register_anonymous helpers. None of that had an API reference entry. --- README.md | 2 +- docs/api_reference/index.rst | 1 + docs/api_reference/types.rst | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docs/api_reference/types.rst diff --git a/README.md b/README.md index 81df4fe64..cddb66fc0 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ Python-graphblas requires `numba` which enables compiling user-defined Python fu Example customized UnaryOp: ```python -from graphblas import unary +from graphblas import unary, Vector def force_odd_func(x): if x % 2 == 0: diff --git a/docs/api_reference/index.rst b/docs/api_reference/index.rst index 84e7d65eb..9f47622aa 100644 --- a/docs/api_reference/index.rst +++ b/docs/api_reference/index.rst @@ -8,6 +8,7 @@ API Reference :maxdepth: 2 collections + types operators io exceptions diff --git a/docs/api_reference/types.rst b/docs/api_reference/types.rst new file mode 100644 index 000000000..e8187caa5 --- /dev/null +++ b/docs/api_reference/types.rst @@ -0,0 +1,22 @@ +Types +----- + +DataType +~~~~~~~~ + +The object returned by :func:`~graphblas.dtypes.register_new` and +:func:`~graphblas.dtypes.register_anonymous`, and carried by every collection's +``.dtype``. The JIT introspection properties below report what SuiteSparse +actually registered for a user-defined type; see :doc:`../user_guide/udt` for +the operator-side counterparts on a typed operator (``op.jit_c_name``, +``op.jit_c_source``). + +.. autoclass:: graphblas.dtypes.DataType() + :members: jit_c_name, jit_c_definition + +Registering user-defined types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: graphblas.dtypes.register_new + +.. autofunction:: graphblas.dtypes.register_anonymous From 301c5be524f16747f2fcb034f6c65afc72ace745 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 31 Jul 2026 14:08:10 -0500 Subject: [PATCH 2/8] Document the from_coo shape-inference gotcha and lazy backend init Matrix.from_coo and Vector.from_coo infer the shape from the largest index when nrows/ncols/size are omitted, so trailing all-empty rows, columns, or positions are silently dropped. Add a warning to both docstrings pointing at the explicit nrows/ncols/size fix. Also a note on the module-level backend global that reading gb.backend does not trigger initialization: it is a plain global, not a special attribute, so it stays None on a fresh import until a special-attribute access or init() runs. --- graphblas/__init__.py | 4 ++++ graphblas/core/matrix.py | 6 ++++++ graphblas/core/vector.py | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/graphblas/__init__.py b/graphblas/__init__.py index e5038563e..e15604058 100644 --- a/graphblas/__init__.py +++ b/graphblas/__init__.py @@ -74,6 +74,10 @@ def __setattr__(self, key, value): config = get_config() del get_config +# None until a backend is initialized. Touching a special attribute such as +# gb.Matrix auto-initializes, as does an explicit init(), and sets this to +# "suitesparse" or "suitesparse-vanilla". Reading gb.backend is not itself a +# special-attribute access, so it never triggers initialization. backend = None _init_params = None _SPECIAL_ATTRS = { diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 3abdcd124..aed155e99 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -876,6 +876,12 @@ def from_coo( ): """Create a new Matrix from row and column indices and values. + .. warning:: + When ``nrows`` or ``ncols`` is omitted, the shape is inferred from + the largest row or column index, so trailing all-empty rows or + columns are dropped. Pass ``nrows`` and ``ncols`` explicitly to pin + the shape. + Parameters ---------- rows : list or np.ndarray diff --git a/graphblas/core/vector.py b/graphblas/core/vector.py index bb9bea32a..1a0e40325 100644 --- a/graphblas/core/vector.py +++ b/graphblas/core/vector.py @@ -700,6 +700,11 @@ def get(self, index, default=None): def from_coo(cls, indices, values=1.0, dtype=None, *, size=None, dup_op=None, name=None): """Create a new Vector from indices and values. + .. warning:: + When ``size`` is omitted, it is inferred from the largest index, so + trailing empty positions are dropped. Pass ``size`` explicitly to + pin the length. + Parameters ---------- indices : list or np.ndarray From a9c99340bd770513c5a3cdb94635df5f2c268dcb Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 31 Jul 2026 14:08:10 -0500 Subject: [PATCH 3/8] Document the plus_first and plus_second semirings (GH #497) Add a user-guide subsection for plus_first and plus_second. They sum one operand's values while ignoring the other's, so when one operand is a boolean or iso adjacency they skip a plus_times multiply that would only multiply by one or rescale every term by the same constant. On a 0/1 matrix plus_first counts length-two paths. Includes a runnable example and lists both among the common semirings. --- docs/user_guide/operators.rst | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/user_guide/operators.rst b/docs/user_guide/operators.rst index fb827afe3..b9b01ff04 100644 --- a/docs/user_guide/operators.rst +++ b/docs/user_guide/operators.rst @@ -149,6 +149,8 @@ Common semirings are: - **min_second** - **max_first** - **max_second** + - **plus_first** (sum the left operand's values over the connection pattern) + - **plus_second** (sum the right operand's values over the connection pattern) - **plus_min** - **lor_land** - **land_lor** @@ -156,6 +158,42 @@ Common semirings are: Semirings are located in the ``graphblas.semiring`` namespace. Additional semirings registered from numpy are located in ``graphblas.semiring.numpy``. +The ``first`` and ``second`` semirings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``first`` binary operator returns its left input and ``second`` returns its right input, +each ignoring the other. Combined with the ``plus`` monoid they give two semirings that read +the values of only one input during a multiply: + + - ``plus_first``: for ``C << A.mxm(B, semiring.plus_first)``, each ``C[i, k]`` is the sum of + ``A[i, j]`` over the ``j`` where both ``A[i, j]`` and ``B[j, k]`` are present. ``B`` + contributes only its structure; its stored values are never read. + - ``plus_second``: the mirror image. Each ``C[i, k]`` sums ``B[j, k]`` over those same ``j``, + and ``A`` contributes only its structure. + +These are the natural choice when one operand is a boolean or *iso* (single-valued) adjacency +matrix, where a ``plus_times`` multiply either multiplies by one (a no-op) or rescales every +term by the same constant. ``first`` and ``second`` skip the product and accumulate one side's +values directly over the connection pattern. On a 0/1 matrix, ``plus_first`` counts the +length-two paths between each pair of nodes. + +.. code-block:: python + + from graphblas import Matrix, semiring + + # A: 0->1 (10), 0->2 (20); B: 1->0 (3), 2->0 (7) + # A @ B reaches node 0 from node 0 through j = 1 and j = 2. + A = Matrix.from_coo([0, 0], [1, 2], [10, 20], nrows=3, ncols=3) + B = Matrix.from_coo([1, 2], [0, 0], [3, 7], nrows=3, ncols=3) + + A.mxm(B, semiring.plus_first).new() # C[0, 0] == 30 (10 + 20, taken from A) + A.mxm(B, semiring.plus_second).new() # C[0, 0] == 10 ( 3 + 7, taken from B) + A.mxm(B, semiring.plus_times).new() # C[0, 0] == 170 (10*3 + 20*7) + +The same ``first`` / ``second`` choice pairs with other monoids: ``min_first`` and ``min_second`` +(listed above) carry a value along an edge without combining it, the pattern behind +label-propagation traversals. + IndexUnary Operators -------------------- From 278cc47c7d87d3953cfe2c8b97437aed505de6d4 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 31 Jul 2026 14:08:10 -0500 Subject: [PATCH 4/8] Add missing entries to the API reference (GH #410, #541) Document six public symbols absent from the API reference: IndexBinaryOp and Aggregator (operators), viz.spy and viz.datashade (visualization), and graphblas.init and graphblas.Recorder in a new utilities page wired into the reference toctree. The io converters and the collection classes' methods were already covered by existing autoclass :members: entries. --- docs/api_reference/index.rst | 1 + docs/api_reference/io.rst | 4 ++++ docs/api_reference/operators.rst | 12 ++++++++++++ docs/api_reference/utilities.rst | 13 +++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 docs/api_reference/utilities.rst diff --git a/docs/api_reference/index.rst b/docs/api_reference/index.rst index 9f47622aa..219e19bae 100644 --- a/docs/api_reference/index.rst +++ b/docs/api_reference/index.rst @@ -11,4 +11,5 @@ API Reference types operators io + utilities exceptions diff --git a/docs/api_reference/io.rst b/docs/api_reference/io.rst index 1cfc98516..9c8b8e2cb 100644 --- a/docs/api_reference/io.rst +++ b/docs/api_reference/io.rst @@ -73,3 +73,7 @@ Visualization ~~~~~~~~~~~~~ .. autofunction:: graphblas.viz.draw + +.. autofunction:: graphblas.viz.spy + +.. autofunction:: graphblas.viz.datashade diff --git a/docs/api_reference/operators.rst b/docs/api_reference/operators.rst index 8836bb638..6853831cd 100644 --- a/docs/api_reference/operators.rst +++ b/docs/api_reference/operators.rst @@ -31,8 +31,20 @@ IndexUnaryOp .. autoclass:: graphblas.core.operator.IndexUnaryOp() :members: +IndexBinaryOp +~~~~~~~~~~~~~ + +.. autoclass:: graphblas.core.operator.IndexBinaryOp() + :members: + SelectOp ~~~~~~~~ .. autoclass:: graphblas.core.operator.SelectOp() :members: + +Aggregator +~~~~~~~~~~ + +.. autoclass:: graphblas.core.operator.Aggregator() + :members: diff --git a/docs/api_reference/utilities.rst b/docs/api_reference/utilities.rst new file mode 100644 index 000000000..8e6e10486 --- /dev/null +++ b/docs/api_reference/utilities.rst @@ -0,0 +1,13 @@ +Utilities +--------- + +Initialization +~~~~~~~~~~~~~~~ + +.. autofunction:: graphblas.init + +Recorder +~~~~~~~~ + +.. autoclass:: graphblas.Recorder + :members: From a53f21716b0f97505a92bafa9789b1ea2c1a4ddc Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 31 Jul 2026 14:08:10 -0500 Subject: [PATCH 5/8] Add a class docstring to Aggregator The Aggregator class had no docstring, so its API reference entry rendered with no description. Add a numpydoc class docstring in the style of the sibling operator classes: what an aggregator is (a reduction operator for reduce, reduce_rowwise, reduce_columnwise, reduce_scalar), the graphblas.agg namespace, the summary-versus-position split, and the note that an aggregator is composed from a monoid or semiring plus an optional finalize step per dtype rather than being a single GraphBLAS object. The position aggregators are named as agg.ss.argmin and friends. The bare agg.argmin spellings still resolve but raise DeprecationWarning pointing at agg.ss, so a new docstring should not teach them. Docstring-only, no code change. --- graphblas/core/operator/agg.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/graphblas/core/operator/agg.py b/graphblas/core/operator/agg.py index 7afeb9e46..574f4ec1b 100644 --- a/graphblas/core/operator/agg.py +++ b/graphblas/core/operator/agg.py @@ -29,6 +29,27 @@ def _get_types(ops, initdtype): class Aggregator: + """A reduction operator that collapses the values of a Matrix or Vector. + + An Aggregator is used with the ``reduce`` family of methods: ``Vector.reduce``, + ``Matrix.reduce_rowwise``, ``Matrix.reduce_columnwise``, and ``Matrix.reduce_scalar``. + Some aggregators return a summary value (``sum``, ``mean``, ``max``); others return a + position (``ss.argmin``, ``ss.argmax``, ``ss.first_index``). + + Built-in aggregators live in the ``graphblas.agg`` namespace, such as ``agg.sum``, + ``agg.mean``, and ``agg.count``. The position aggregators are SuiteSparse-specific and + live under ``agg.ss``; the bare ``agg.argmin`` spellings are deprecated. Unlike the + other operators, an Aggregator is not a single GraphBLAS object; many are built from a + monoid or semiring plus an optional finalize step, composed per dtype on first use. + + Examples + -------- + >>> import graphblas as gb + >>> v = gb.Vector.from_coo([0, 1, 2], [1, 2, 3]) + >>> int(v.reduce(gb.agg.sum).new()) + 6 + """ + opclass = "Aggregator" def __init__( From c80cfa58b52242799d63a66eca6e7b0192d741c7 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 31 Jul 2026 14:08:10 -0500 Subject: [PATCH 6/8] Fix a README constructor typo and a run-on apply error message The README's "Creating new Vectors / Matrices" block showed Matrix.new(dtype, num_rows, num_cols). There is no Matrix.new; the constructor is Matrix(dtype, num_rows, num_cols). Vector.apply built its error message from two adjacent string literals with no space at the join, so an invalid op reported "... `right` scalaror IndexUnaryOp with `right` thunk." --- README.md | 2 +- graphblas/core/vector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cddb66fc0..3b84afef2 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ s(accum) << v.reduce(op) ## Creating new Vectors / Matrices ```python -A = Matrix.new(dtype, num_rows, num_cols) # new_type +A = Matrix(dtype, num_rows, num_cols) # new_type B = A.dup() # dup A = Matrix.from_coo([row_indices], [col_indices], [values]) # build ``` diff --git a/graphblas/core/vector.py b/graphblas/core/vector.py index 1a0e40325..5ea058e7c 100644 --- a/graphblas/core/vector.py +++ b/graphblas/core/vector.py @@ -1417,7 +1417,7 @@ def apply(self, op, right=None, *, left=None): method_name = "apply" right_literal = None extra_message = ( - "apply only accepts UnaryOp with no scalars or BinaryOp with `left` or `right` scalar" + "apply only accepts UnaryOp with no scalars or BinaryOp with `left` or `right` scalar " "or IndexUnaryOp with `right` thunk." ) if isinstance(op, str): From 6805b1f57ede4da1efd2096ededaba0af01f7f64 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 28 Oct 2026 01:13:44 -0500 Subject: [PATCH 7/8] Say what Matrix.wait does in blocking mode The docstring said wait has no effect in blocking mode. With SuiteSparse:GraphBLAS, GrB_Matrix_wait with GrB_MATERIALIZE also builds the hyper_hash of a hypersparse matrix that has none yet, in blocking mode too: results of operations already have one, but a matrix from from_coo or an import may not. SuiteSparse 10.5 builds it only when the number of non-empty vectors exceeds GxB_HYPER_HASH (1024 by default; 1025 builds it, 1024 does not), and 8.2 for any hypersparse matrix. It took 0.7 to 1 s on one thread for a hypersparse matrix the size of the FROSTT NELL-2 tensor's matricization. Matrix.ss.unpack_hyperhash(compute=True) relies on it. wait("complete") never builds it. Vector.wait and Scalar.wait keep the sentence, which holds for them. --- graphblas/core/matrix.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index aed155e99..365f36527 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -822,7 +822,11 @@ def wait(self, how="materialize"): the computations may be delayed and not yet safe to use by multiple threads. Use wait to force completion of the Matrix. - Has no effect in `blocking mode <../user_guide/init.html#graphblas-modes>`__. + In `blocking mode <../user_guide/init.html#graphblas-modes>`__, computations are + already complete, but with SuiteSparse:GraphBLAS, "materialize" may still build the + hyper_hash of a hypersparse Matrix that lacks one, such as one from ``from_coo`` or + an import (on recent versions, only with more non-empty rows or columns than the + ``GxB_HYPER_HASH`` setting, 1024 by default); see ``Matrix.ss.unpack_hyperhash``. """ how = how.lower() From f4c95c1e5f300ffe5a9d520566ea6f392555d3ea Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 3 Nov 2026 11:38:31 -0800 Subject: [PATCH 8/8] Say that plus_first on two boolean matrices needs an integer type plus has no BOOL form, so A.mxm(A, semiring.plus_first) on a boolean adjacency matrix raises KeyError; semiring.plus_first[int] counts the paths. Found by review (rmisc1). --- docs/user_guide/operators.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user_guide/operators.rst b/docs/user_guide/operators.rst index b9b01ff04..3a3b2845a 100644 --- a/docs/user_guide/operators.rst +++ b/docs/user_guide/operators.rst @@ -175,7 +175,8 @@ These are the natural choice when one operand is a boolean or *iso* (single-valu matrix, where a ``plus_times`` multiply either multiplies by one (a no-op) or rescales every term by the same constant. ``first`` and ``second`` skip the product and accumulate one side's values directly over the connection pattern. On a 0/1 matrix, ``plus_first`` counts the -length-two paths between each pair of nodes. +length-two paths between each pair of nodes. ``plus`` has no BOOL form, so for two boolean +matrices give the semiring an integer type, as in ``A.mxm(A, semiring.plus_first[int])``. .. code-block:: python