Repository: python-graphblas/python-graphblas (language: python)

Issue:
Add implementation-specific descriptors
SuiteSparse:GraphBLAS includes many extensions to the official GraphBLAS specification, and one can reasonably expect other implementations (a.k.a. backends) to offer their own (possibly different) additions.  We should find a non-intrusive yet clear way to support these.

When creating new objects, we can accept `**backend_kwargs` where valid keywords are determined and handled by the specific backend.  For example, when creating a new matrix, we may have `ss_format='by_row'`.  In this example, we have all SuiteSparse extension keywords begin with `ss_`.

Similarly, backends may want to have additional methods or properties.  For SuiteSparse, this may look like `A.ss.select(...)`, `A.ss.format`, `A.ss.pattern`, etc.


Discussion / hints:
@jim22k discussed this briefly today, and we think global and object-specific options should probably be MutableMapping on `.ss` namespaces, such as `A.ss.config` (object-specific) and `gb.ss.config` (global) (or `ss.options`).  `config.__doc__`/`config?`/`help(config)` should describe the options and list valid values for each option.  Setting values should normal Python objects such as int, float, or str, which we _may_ need to map to a C enum.  When creating new objects, perhaps a mapping could be provided via e.g. `**kwargs` or `config={...}` to set the config options.

It _may_ be nicest to specify additional descriptors (such as AxB_method, nthreads, chunk, gpu_control, etc.) with a context manager.
The global SuiteSparse config--`gb.ss.config`--added in https://github.com/python-graphblas/python-graphblas/pull/258 is able to globally set _some_ of the SuiteSparse descriptor fields, such as nthreads and chunk, which is great, but doesn't completely solve (i.e., expose) everything.

Here's an idea.  Let's allow passing an `Options` object on the left-hand side.  For example
```python
A(Options(nthreads=4, AxB_method="gustavson", sort=False)) << B @ C
```
Two nice things about an object is it has clear intent and can be reused, such as
```python
opts = Options(nthreads=4, AxB_method="gustavson", sort=False)
A(opts) << B @ C
```
I think it would be sufficient to allow this only when updating an existing object.  That is, I don't think we need to add `options=None` or `**kwargs` to every `.new` method (but we could if/when there is sufficient need, and I would prefer `options=` over `**kwargs`).

`Options` would need to be backend-specific, and a vanilla implementation has no available options.

`Options` may not be the best name.  `Control`? `Config`? Although it sets the options via a descriptor, I don't think `Descriptor` is a good name for this.

An alternative approach is to use a `dict` argument or `**kwargs` instead of `Options`.  I don't like `**kwargs` anymore, b/c it's too magical and potentially confusing.  Doing `A(options={"nthreads": 4}) << expr` is reasonable, b/c `options=` is called out, but one would be tempted to write `A({"nthreads": 4}) << expr`, which is less clear, but may be okay.

Adding this will allow anybody to easily tinker with SuiteSparse:GraphBLAS options, such as nthreads, AxB_method, sort, GPU control (when ready), etc.
@jim22k and I discussed this today, and we think adding `**opts` where descriptors are used is the cleanest way to support this. Examples:
- `C(AxB_method="gustavson") << A @ B`
- `times(A & B).new(nthreads=4)`

We should raise if there is a keyword argument that the backend is unable to handle. We should also defer to the backend regarding which descriptors should be used for which call (i.e., the backend is free to ignore or raise on extraneous descriptors options).

Fix patch:
diff --git a/graphblas/core/agg.py b/graphblas/core/agg.py
index d8ce9a168..97e48a14c 100644
--- a/graphblas/core/agg.py
+++ b/graphblas/core/agg.py
@@ -163,6 +163,7 @@ def _new(self, updater, expr, *, in_composite=False):
                 return parent._as_vector()
             return
 
+        opts = updater.opts
         if agg._composite is not None:
             # Masks are applied throughout the aggregation, including composite aggregations.
             # Aggregations done while `in_composite is True` should return the updater parent
@@ -173,14 +174,14 @@ def _new(self, updater, expr, *, in_composite=False):
             for cur_agg in agg._composite:
                 cur_agg = cur_agg[self.type]  # Hopefully works well enough
                 arg = expr.construct_output(cur_agg.return_type)
-                results.append(cur_agg._new(arg(mask=mask), expr, in_composite=True))
-            final_expr = agg._finalize(*results)
+                results.append(cur_agg._new(arg(mask=mask, **opts), expr, in_composite=True))
+            final_expr = agg._finalize(*results, opts)
             if expr.cfunc_name == "GrB_Matrix_reduce_Aggregator":
                 updater << final_expr
             elif expr.cfunc_name.startswith("GrB_Vector_reduce") or expr.cfunc_name.startswith(
                 "GrB_Matrix_reduce"
             ):
-                final = final_expr.new()
+                final = final_expr.new(**opts)
                 updater << final[0]
             else:
                 raise NotImplementedError(f"{agg.name} with {expr.cfunc_name}")
@@ -192,7 +193,7 @@ def _new(self, updater, expr, *, in_composite=False):
             return
 
         if agg._custom is not None:
-            return agg._custom(self, updater, expr, in_composite=in_composite)
+            return agg._custom(self, updater, expr, opts, in_composite=in_composite)
 
         semiring = get_typed_op(agg._semiring, self.type, agg._initdtype)
         if expr.cfunc_name == "GrB_Matrix_reduce_Aggregator":
@@ -201,12 +202,12 @@ def _new(self, updater, expr, *, in_composite=False):
             orig_updater = updater
             if agg._finalize is not None:
                 step1 = expr.construct_output(semiring.return_type)
-                updater = step1(mask=updater.kwargs.get("mask"))
+                updater = step1(mask=updater.kwargs.get("mask"), **opts)
             if expr.method_name == "reduce_columnwise":
                 A = A.T
             size = A._ncols
             init = expr._new_vector(agg._initdtype, size=size)
-            init[...] = agg._initval  # O(1) dense vector in SuiteSparse 5
+            init(**opts)[...] = agg._initval  # O(1) dense vector in SuiteSparse 5
             if agg._switch:
                 updater << semiring(init @ A.T)
             else:
@@ -220,17 +221,17 @@ def _new(self, updater, expr, *, in_composite=False):
             v = expr.args[0]
             step1 = expr._new_vector(semiring.return_type, size=1)
             init = expr._new_matrix(agg._initdtype, nrows=v._size, ncols=1)
-            init[...] = agg._initval  # O(1) dense column vector in SuiteSparse 5
+            init(**opts)[...] = agg._initval  # O(1) dense column vector in SuiteSparse 5
             if agg._switch:
-                step1 << semiring(init.T @ v)
+                step1(**opts) << semiring(init.T @ v)
             else:
-                step1 << semiring(v @ init)
+                step1(**opts) << semiring(v @ init)
             if agg._finalize is not None:
                 finalize = agg._finalize[semiring.return_type]
                 if step1.dtype == finalize.return_type:
-                    step1 << finalize(step1)
+                    step1(**opts) << finalize(step1)
                 else:
-                    step1 = finalize(step1).new(finalize.return_type)
+                    step1 = finalize(step1).new(finalize.return_type, **opts)
             if in_composite:
                 return step1
             updater << step1[0]
@@ -241,23 +242,23 @@ def _new(self, updater, expr, *, in_composite=False):
             # This has not been benchmarked or optimized.
             # We may be able to intelligently choose the faster path.
             init1 = expr._new_vector(agg._initdtype, size=A._ncols)
-            init1[...] = agg._initval  # O(1) dense vector in SuiteSparse 5
+            init1(**opts)[...] = agg._initval  # O(1) dense vector in SuiteSparse 5
             step1 = expr._new_vector(semiring.return_type, size=A._nrows)
             if agg._switch:
-                step1 << semiring(init1 @ A.T)
+                step1(**opts) << semiring(init1 @ A.T)
             else:
-                step1 << semiring(A @ init1)
+                step1(**opts) << semiring(A @ init1)
             init2 = expr._new_matrix(agg._initdtype, nrows=A._nrows, ncols=1)
-            init2[...] = agg._initval  # O(1) dense vector in SuiteSparse 5
+            init2(**opts)[...] = agg._initval  # O(1) dense vector in SuiteSparse 5
             semiring2 = agg._semiring2[semiring.return_type]
             step2 = expr._new_vector(semiring2.return_type, size=1)
-            step2 << semiring2(step1 @ init2)
+            step2(**opts) << semiring2(step1 @ init2)
             if agg._finalize is not None:
                 finalize = agg._finalize[semiring2.return_type]
                 if step2.dtype == finalize.return_type:
                     step2 << finalize(step2)
                 else:
-                    step2 = finalize(step2).new(finalize.return_type)
+                    step2 = finalize(step2).new(finalize.return_type, **opts)
             if in_composite:
                 return step2
             updater << step2[0]
@@ -342,53 +343,53 @@ def __reduce__(self):
 
 
 # Composite
-def _mean_finalize(c, x):
+def _mean_finalize(c, x, opts):
     return binary.truediv(x & c)
 
 
-def _ptp_finalize(max, min):
+def _ptp_finalize(max, min, opts):
     return binary.minus(max & min)
 
 
-def _varp_finalize(c, x, x2):
+def _varp_finalize(c, x, x2, opts):
     # <x2> / n - (<x> / n)**2
-    left = binary.truediv(x2 & c).new()
-    right = binary.truediv(x & c).new()
-    right << binary.pow(right, 2)
+    left = binary.truediv(x2 & c).new(**opts)
+    right = binary.truediv(x & c).new(**opts)
+    right(**opts) << binary.pow(right, 2)
     return binary.minus(left & right)
 
 
-def _vars_finalize(c, x, x2):
+def _vars_finalize(c, x, x2, opts):
     # <x2> / (n-1) - <x>**2 / (n * (n-1))
-    x << binary.pow(x, 2)
-    right = binary.truediv(x & c).new()
-    c << binary.minus(c, 1)
-    right << binary.truediv(right & c)
-    left = binary.truediv(x2 & c).new()
+    x(**opts) << binary.pow(x, 2)
+    right = binary.truediv(x & c).new(**opts)
+    c(**opts) << binary.minus(c, 1)
+    right(**opts) << binary.truediv(right & c)
+    left = binary.truediv(x2 & c).new(**opts)
     return binary.minus(left & right)
 
 
-def _stdp_finalize(c, x, x2):
-    val = _varp_finalize(c, x, x2).new()
+def _stdp_finalize(c, x, x2, opts):
+    val = _varp_finalize(c, x, x2, opts).new(**opts)
     return unary.sqrt(val)
 
 
-def _stds_finalize(c, x, x2):
-    val = _vars_finalize(c, x, x2).new()
+def _stds_finalize(c, x, x2, opts):
+    val = _vars_finalize(c, x, x2, opts).new(**opts)
     return unary.sqrt(val)
 
 
-def _geometric_mean_finalize(c, x):
-    right = unary.minv["FP64"](c).new()
+def _geometric_mean_finalize(c, x, opts):
+    right = unary.minv["FP64"](c).new(**opts)
     return binary.pow(x & right)
 
 
-def _harmonic_mean_finalize(c, x):
+def _harmonic_mean_finalize(c, x, opts):
     return binary.truediv(c & x)
 
 
-def _root_mean_square_finalize(c, x2):
-    val = binary.truediv(x2 & c).new()
+def _root_mean_square_finalize(c, x2, opts):
+    val = binary.truediv(x2 & c).new(**opts)
     return unary.sqrt(val)
 
 
@@ -453,6 +454,7 @@ def _argminmaxij(
     agg,
     updater,
     expr,
+    opts,
     *,
     in_composite,
     monoid,
@@ -462,37 +464,37 @@ def _argminmaxij(
     if expr.cfunc_name == "GrB_Matrix_reduce_Aggregator":
         A = expr.args[0]
         if expr.method_name == "reduce_rowwise":
-            step1 = A.reduce_rowwise(monoid).new()
+            step1 = A.reduce_rowwise(monoid).new(**opts)
 
             D = step1.diag()
 
-            masked = semiring.any_eq(D @ A).new()
-            masked(mask=masked.V, replace=True) << masked  # Could use select
+            masked = semiring.any_eq(D @ A).new(**opts)
+            masked(mask=masked.V, replace=True, **opts) << masked  # Could use select
             init = expr._new_vector(bool, size=A._ncols)
-            init[...] = False  # O(1) dense vector in SuiteSparse 5
+            init(**opts)[...] = False  # O(1) dense vector in SuiteSparse 5
             updater << row_semiring(masked @ init)
             if in_composite:
                 return updater.parent
         else:
-            step1 = A.reduce_columnwise(monoid).new()
+            step1 = A.reduce_columnwise(monoid).new(**opts)
 
             D = step1.diag()
 
-            masked = semiring.any_eq(A @ D).new()
-            masked(mask=masked.V, replace=True) << masked  # Could use select
+            masked = semiring.any_eq(A @ D).new(**opts)
+            masked(mask=masked.V, replace=True, **opts) << masked  # Could use select
             init = expr._new_vector(bool, size=A._nrows)
-            init[...] = False  # O(1) dense vector in SuiteSparse 5
+            init(**opts)[...] = False  # O(1) dense vector in SuiteSparse 5
             updater << col_semiring(init @ masked)
             if in_composite:
                 return updater.parent
     elif expr.cfunc_name.startswith("GrB_Vector_reduce"):
         v = expr.args[0]
-        step1 = v.reduce(monoid, allow_empty=False).new()
-        masked = binary.eq(v, step1).new()
-        masked(mask=masked.V, replace=True) << masked  # Could use select
+        step1 = v.reduce(monoid, allow_empty=False).new(**opts)
+        masked = binary.eq(v, step1).new(**opts)
+        masked(mask=masked.V, replace=True, **opts) << masked  # Could use select
         init = expr._new_matrix(bool, nrows=v._size, ncols=1)
-        init[...] = False  # O(1) dense column vector in SuiteSparse 5
-        step2 = col_semiring(masked @ init).new()
+        init(**opts)[...] = False  # O(1) dense column vector in SuiteSparse 5
+        step2 = col_semiring(masked @ init).new(**opts)
         if in_composite:
             return step2
         updater << step2[0]
@@ -500,13 +502,14 @@ def _argminmaxij(
         raise NotImplementedError(f"{agg.name} with {expr.cfunc_name}")
 
 
-def _argminmax(agg, updater, expr, *, in_composite, monoid):
+def _argminmax(agg, updater, expr, opts, *, in_composite, monoid):
     if expr.cfunc_name == "GrB_Matrix_reduce_Aggregator":
         if expr.method_name == "reduce_rowwise":
             return _argminmaxij(
                 agg,
                 updater,
                 expr,
+                opts,
                 in_composite=in_composite,
                 monoid=monoid,
                 row_semiring=semiring.min_firstj,
@@ -516,6 +519,7 @@ def _argminmax(agg, updater, expr, *, in_composite, monoid):
             agg,
             updater,
             expr,
+            opts,
             in_composite=in_composite,
             monoid=monoid,
             row_semiring=semiring.min_firsti,
@@ -526,6 +530,7 @@ def _argminmax(agg, updater, expr, *, in_composite, monoid):
             agg,
             updater,
             expr,
+            opts,
             in_composite=in_composite,
             monoid=monoid,
             row_semiring=semiring.min_firsti,
@@ -549,20 +554,20 @@ def _argminmax(agg, updater, expr, *, in_composite, monoid):
 )
 
 
-def _first_last(agg, updater, expr, *, in_composite, semiring_):
+def _first_last(agg, updater, expr, opts, *, in_composite, semiring_):
     if expr.cfunc_name == "GrB_Matrix_reduce_Aggregator":
         A = expr.args[0]
         if expr.method_name == "reduce_columnwise":
             A = A.T
         init = expr._new_vector(bool, size=A._ncols)
-        init[...] = False  # O(1) dense vector in SuiteSparse 5
-        step1 = semiring_(A @ init).new()
+        init(**opts)[...] = False  # O(1) dense vector in SuiteSparse 5
+        step1 = semiring_(A @ init).new(**opts)
         Is, Js = step1.to_coo()
 
         Matrix_ = type(expr._new_matrix(bool))
         P = Matrix_.from_coo(Js, Is, 1, nrows=A._ncols, ncols=A._nrows)
         mask = step1.diag()
-        result = semiring.any_first(A @ P).new(mask=mask.S).diag()
+        result = semiring.any_first(A @ P).new(mask=mask.S, **opts).diag(**opts)
 
         updater << result
         if in_composite:
@@ -570,23 +575,23 @@ def _first_last(agg, updater, expr, *, in_composite, semiring_):
     elif expr.cfunc_name.startswith("GrB_Vector_reduce"):
         v = expr.args[0]
         init = expr._new_matrix(bool, nrows=v._size, ncols=1)
-        init[...] = False  # O(1) dense matrix in SuiteSparse 5
-        step1 = semiring_(v @ init).new()
+        init(**opts)[...] = False  # O(1) dense matrix in SuiteSparse 5
+        step1 = semiring_(v @ init).new(**opts)
         index = step1[0].new().value
         # `==` instead of `is` automatically triggers index.compute() in dask-graphblas:
         if index == None:  # noqa
             index = 0
         if in_composite:
-            return v[[index]].new()
+            return v[[index]].new(**opts)
         updater << v[index]
     else:  # GrB_Matrix_reduce
         A = expr.args[0]
         init1 = expr._new_matrix(bool, nrows=A._ncols, ncols=1)
-        init1[...] = False  # O(1) dense matrix in SuiteSparse 5
-        step1 = semiring_(A @ init1).new()
+        init1(**opts)[...] = False  # O(1) dense matrix in SuiteSparse 5
+        step1 = semiring_(A @ init1).new(**opts)
         init2 = expr._new_vector(bool, size=A._nrows)
-        init2[...] = False  # O(1) dense vector in SuiteSparse 5
-        step2 = semiring_(step1.T @ init2).new()
+        init2(**opts)[...] = False  # O(1) dense vector in SuiteSparse 5
+        step2 = semiring_(step1.T @ init2).new(**opts)
         i = step2[0].new().value
         # `==` instead of `is` automatically triggers i.compute() in dask-graphblas:
         if i == None:  # noqa
@@ -594,7 +599,7 @@ def _first_last(agg, updater, expr, *, in_composite, semiring_):
         else:
             j = step1[i, 0].new().value
         if in_composite:
-            return A[i, [j]].new()
+            return A[i, [j]].new(**opts)
         updater << A[i, j]
 
 
@@ -612,13 +617,13 @@ def _first_last(agg, updater, expr, *, in_composite, semiring_):
 )
 
 
-def _first_last_index(agg, updater, expr, *, in_composite, semiring):
+def _first_last_index(agg, updater, expr, opts, *, in_composite, semiring):
     if expr.cfunc_name == "GrB_Matrix_reduce_Aggregator":
         A = expr.args[0]
         if expr.method_name == "reduce_columnwise":
             A = A.T
         init = expr._new_vector(bool, size=A._ncols)
-        init[...] = False  # O(1) dense vector in SuiteSparse 5
+        init(**opts)[...] = False  # O(1) dense vector in SuiteSparse 5
         expr = semiring(A @ init)
         updater << expr
         if in_composite:
@@ -626,8 +631,8 @@ def _first_last_index(agg, updater, expr, *, in_composite, semiring):
     elif expr.cfunc_name.startswith("GrB_Vector_reduce"):
         v = expr.args[0]
         init = expr._new_matrix(bool, nrows=v._size, ncols=1)
-        init[...] = False  # O(1) dense matrix in SuiteSparse 5
-        step1 = semiring(v @ init).new()
+        init(**opts)[...] = False  # O(1) dense matrix in SuiteSparse 5
+        step1 = semiring(v @ init).new(**opts)
         if in_composite:
             return step1
         updater << step1[0]
diff --git a/graphblas/core/base.py b/graphblas/core/base.py
index 832a93db9..6ad9b0175 100644
--- a/graphblas/core/base.py
+++ b/graphblas/core/base.py
@@ -189,7 +189,13 @@ class BaseType:
     _is_scalar = False
 
     def __call__(
-        self, *optional_mask_accum_replace, mask=None, accum=None, replace=False, input_mask=None
+        self,
+        *optional_mask_accum_replace,
+        mask=None,
+        accum=None,
+        replace=False,
+        input_mask=None,
+        **opts,
     ):
         # Pick out mask and accum from positional arguments
         mask_arg = None
@@ -251,7 +257,9 @@ def __call__(
                 accum = accum.binaryop
             else:
                 self._expect_op(accum, "BinaryOp", within="__call__", keyword_name="accum")
-        return Updater(self, mask=mask, accum=accum, replace=replace, input_mask=input_mask)
+        return Updater(
+            self, mask=mask, accum=accum, replace=replace, input_mask=input_mask, opts=opts
+        )
 
     def __or__(self, other):
         if self._is_scalar:
@@ -307,16 +315,16 @@ def __bool__(self):
             "Perhaps use .nvals attribute instead."
         )
 
-    def __lshift__(self, expr):
-        return self._update(expr)
+    def __lshift__(self, expr, **opts):
+        return self._update(expr, opts=opts)
 
-    def update(self, expr):
+    def update(self, expr, **opts):
         """
         Convenience function when no output arguments (mask, accum, replace) are used
         """
-        return self._update(expr)
+        return self._update(expr, opts=opts)
 
-    def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None):
+    def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None, *, opts):
         if not isinstance(expr, BaseExpression):
             if isinstance(expr, AmbiguousAssignOrExtract):
                 if expr._is_scalar and self._is_scalar:
@@ -327,7 +335,11 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None):
                             "--such as `s(accum=accum) << v[0]`--is not supported"
                         )
                     expr.parent._extract_element(
-                        expr.resolved_indexes, self.dtype, is_cscalar=self._is_cscalar, result=self
+                        expr.resolved_indexes,
+                        self.dtype,
+                        opts,
+                        is_cscalar=self._is_cscalar,
+                        result=self,
                     )
                     return
 
@@ -347,13 +359,18 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None):
                             "Scalar update with accumulation--such as `s(accum=accum) << t`"
                             "--is not supported"
                         )
+                    if opts:
+                        # Ignore opts for now
+                        descriptor_lookup(**opts)
                     self.value = expr
                     return
 
                 # Two choices here: apply identity `expr = expr.apply(identity)`, or assign.
                 # Choose assign for now, since it works better for iso-valued objects.
                 # Perhaps we should benchmark to see which is faster and has less Python overhead.
-                self(mask=mask, accum=accum, replace=replace, input_mask=input_mask)[...] = expr
+                self(mask=mask, accum=accum, replace=replace, input_mask=input_mask, **opts)[
+                    ...
+                ] = expr
                 return
             elif self._is_scalar:
                 from .infix import InfixExprBase
@@ -367,6 +384,9 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None):
                         "--is not supported"
                     )
                 else:
+                    if opts:
+                        # Ignore opts for now
+                        descriptor_lookup(**opts)
                     self.value = expr
                     return
             else:
@@ -403,13 +423,15 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None):
                                 f"{type(self).__name__}, {type(self).__name__}Expression, "
                                 "AmbiguousAssignOrExtract, and scalars."
                             ) from None
-                    updater = self(mask=mask, accum=accum, replace=replace, input_mask=input_mask)
+                    updater = self(
+                        mask=mask, accum=accum, replace=replace, input_mask=input_mask, **opts
+                    )
                     updater[...] = scalar
                     return
 
         if type(self) is not expr.output_type:
             if expr.output_type._is_scalar and config.get("autocompute"):
-                self._update(expr.new(), mask, accum, replace, input_mask)
+                self._update(expr.new(), mask, accum, replace, input_mask, opts=opts)
                 return
             from .scalar import Scalar
 
@@ -420,11 +442,11 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None):
         if input_mask is not None:
             raise TypeError("`input_mask` argument may only be used for extract")
         if expr.op is not None and expr.op.opclass == "Aggregator":
-            updater = self(mask=mask, accum=accum, replace=replace)
+            updater = self(mask=mask, accum=accum, replace=replace, **opts)
             expr.op._new(updater, expr)
             return
         if expr.cfunc_name is None:  # Custom recipe
-            updater = self(mask=mask, accum=accum, replace=replace)
+            updater = self(mask=mask, accum=accum, replace=replace, **opts)
             expr.args[-2](updater, *expr.args[-1])
             return
 
@@ -444,6 +466,7 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None):
             mask_complement=complement,
             mask_structure=structure,
             output_replace=replace,
+            **opts,
         )
         if self._is_scalar:
             scalar_as_vector = expr.method_name == "inner"
@@ -553,33 +576,39 @@ def __init__(
             self.dtype = dtype
         self._value = None
 
-    def new(self, dtype=None, *, mask=None, name=None):
-        return self._new(dtype, mask, name)
+    def new(self, dtype=None, *, mask=None, name=None, **opts):
+        return self._new(dtype, mask, name, **opts)
 
-    def _new(self, dtype, mask, name, **kwargs):
+    def _new(self, dtype, mask, name, is_cscalar=None, **opts):
         if (
             mask is None
             and self._value is not None
             and (dtype is None or self._value.dtype == dtype)
         ):
-            if self._is_scalar and self._value._is_cscalar != kwargs["is_cscalar"]:
-                return self._value.dup(is_cscalar=kwargs["is_cscalar"], name=name)
+            if opts:
+                # Ignore opts for now
+                descriptor_lookup(**opts)
+            if self._is_scalar and self._value._is_cscalar != is_cscalar:
+                return self._value.dup(is_cscalar=is_cscalar, name=name)
             rv = self._value
             if name is not None:
                 rv.name = name
             self._value = None
             return rv
-        output = self.construct_output(dtype, name=name, **kwargs)
+        if is_cscalar is None:
+            output = self.construct_output(dtype, name=name)
+        else:
+            output = self.construct_output(dtype, name=name, is_cscalar=is_cscalar)
         if self.op is not None and self.op.opclass == "Aggregator":
-            updater = output(mask=mask)
+            updater = output(mask=mask, **opts)
             self.op._new(updater, self)
         elif self.cfunc_name is None:  # Custom recipe
-            self.args[-2](output(mask=mask), *self.args[-1])
+            self.args[-2](output(mask=mask, **opts), *self.args[-1])
         elif mask is None:
-            output.update(self)
+            output.update(self, **opts)
         else:
             mask = _check_mask(mask, output)
-            output(mask=mask).update(self)
+            output(mask=mask, **opts).update(self)
         return output
 
     dup = new
diff --git a/graphblas/core/descriptor.py b/graphblas/core/descriptor.py
index 90f3e87e2..1e195e3fe 100644
--- a/graphblas/core/descriptor.py
+++ b/graphblas/core/descriptor.py
@@ -1,30 +1,51 @@
-from ..exceptions import check_status_carg
+import itertools
+
+from .. import backend
+from ..exceptions import check_status, check_status_carg
 from . import NULL, ffi, lib
 
 
 class Descriptor:
+    _name_counter = itertools.count()
+
     def __init__(
         self,
         gb_obj,
-        name,
         output_replace=False,
         mask_complement=False,
         mask_structure=False,
         transpose_first=False,
         transpose_second=False,
+        *,
+        name=None,
     ):
         self.gb_obj = gb_obj
-        self.name = name
+        self.name = f"Desc_{next(Descriptor._name_counter)}" if name is None else name
         self.output_replace = output_replace
         self.mask_complement = mask_complement
         self.mask_structure = mask_structure
         self.transpose_first = transpose_first
         self.transpose_second = transpose_second
 
+    @property
+    def _carg(self):
+        return self.gb_obj[0]
+
+    def __del__(self):
+        gb_obj = getattr(self, "gb_obj", None)
+        if gb_obj is not None and lib is not None:  # pragma: no branch (safety)
+            # it's difficult/dangerous to record the call, b/c `self.name` may not exist
+            check_status(lib.GrB_Descriptor_free(gb_obj), self)
+
+
+class BuiltinDescriptor(Descriptor):
     @property
     def _carg(self):
         return self.gb_obj
 
+    def __del__(self):  # pragma: no cover (safety)
+        pass
+
 
 _desc_names = {
     # OUTP, MSK_COMP, MSK_STRUCT, TRANS0, TRANS1
@@ -60,7 +81,9 @@ def _carg(self):
     (True, True, True, True, False): "GrB_DESC_RSCT0",
     (True, True, True, True, True): "GrB_DESC_RSCT0T1",
 }
-_desc_map = {key: Descriptor(getattr(lib, val), val, *key) for key, val in _desc_names.items()}
+_desc_map = {
+    key: BuiltinDescriptor(getattr(lib, val), *key, name=val) for key, val in _desc_names.items()
+}
 _desc_names[(False, False, False, False, False)] = "NULL"
 _desc_map[(False, False, False, False, False)] = None
 
@@ -72,8 +95,33 @@ def lookup(
     mask_structure=False,
     transpose_first=False,
     transpose_second=False,
-    create=False,
+    **opts,
 ):
+    if opts:
+        # Normalize opts by making all keys lowercase and raise if duplicates
+        new_opts = {key.lower(): val for key, val in opts.items()}
+        if len(opts) != len(new_opts):
+            extra = opts.keys() - new_opts.keys()
+            extra |= opts.keys() & {x.lower() for x in extra}
+            raise ValueError(
+                "Duplicate descriptor options given (descriptor options are case-insensitive): "
+                + ", ".join(sorted(extra, key=lambda x: (x.lower(), x)))
+            )
+        if backend == "suitesparse":
+            from .ss.descriptor import get_descriptor
+
+            return get_descriptor(
+                output_replace=output_replace,
+                mask_complement=mask_complement,
+                mask_structure=mask_structure,
+                transpose_first=transpose_first,
+                transpose_second=transpose_second,
+                **opts,
+            )
+        raise ValueError(
+            f"Extra descriptor options not possible with {backend!r} backend; "
+            f"got {', '.join(str(x) for x in opts)}"
+        )
     key = (
         output_replace,
         mask_complement,
@@ -81,7 +129,7 @@ def lookup(
         transpose_first,
         transpose_second,
     )
-    if create or key not in _desc_map:
+    if key not in _desc_map:  # pragma: no cover (unnecessary)
         # We currently don't need this block of code!
         # All 32 possible descriptors are currently already added to _desc_map.
         # Nevertheless, this code may be useful some day, because we will want
@@ -91,7 +139,7 @@ def lookup(
             # Default descriptor stays a NULL pointer
             desc[0] = NULL
         else:
-            lib.GrB_Descriptor_new(desc)
+            check_status_carg(lib.GrB_Descriptor_new(desc), "Descriptor", desc[0])
             for cond, field, val in [
                 (output_replace, lib.GrB_OUTP, lib.GrB_REPLACE),
                 (mask_complement, lib.GrB_MASK, lib.GrB_COMP),
@@ -103,8 +151,5 @@ def lookup(
                     check_status_carg(
                         lib.GrB_Descriptor_set(desc[0], field, val), "Descriptor", desc[0]
                     )
-        rv = Descriptor(desc[0], "custom_descriptor", *key)
-        if not create:  # pragma: no cover (unnecessary)
-            _desc_map[key] = rv
-        return rv
+        _desc_map[key] = Descriptor(desc, *key)
     return _desc_map[key]
diff --git a/graphblas/core/expr.py b/graphblas/core/expr.py
index 1d847f0de..4c656840b 100644
--- a/graphblas/core/expr.py
+++ b/graphblas/core/expr.py
@@ -3,6 +3,7 @@
 from .. import backend
 from ..dtypes import _INDEX
 from . import lib, utils
+from .descriptor import lookup as descriptor_lookup
 from .utils import _CArray, output_type
 
 
@@ -300,17 +301,17 @@ def __call__(self, *args, **kwargs):
         is_submask = updater.kwargs.get("mask") is not None
         return Assigner(updater, self.resolved_indexes, is_submask=is_submask)
 
-    def __lshift__(self, obj):
+    def __lshift__(self, obj, **opts):
         # Occurs when user calls `C[index] << obj`
-        self.update(obj)
+        self.update(obj, **opts)
 
-    def update(self, obj):
+    def update(self, obj, **opts):
         # Occurs when user calls `C[index].update(obj)`
         if getattr(self.parent, "_is_transposed", False):
             raise TypeError("'TransposedMatrix' object does not support item assignment")
-        Updater(self.parent)._setitem(self.resolved_indexes, obj, is_submask=False)
+        Updater(self.parent, opts=opts)._setitem(self.resolved_indexes, obj, is_submask=False)
 
-    def new(self, dtype=None, *, mask=None, input_mask=None, name=None):
+    def new(self, dtype=None, *, mask=None, input_mask=None, name=None, **opts):
         """
         Force extraction of the indexes into a new object
         dtype and mask are the only controllable parameters.
@@ -321,9 +322,9 @@ def new(self, dtype=None, *, mask=None, input_mask=None, name=None):
             from .base import _check_mask
 
             input_mask = _check_mask(input_mask, self.parent)
-            mask = self._input_mask_to_mask(input_mask)
+            mask = self._input_mask_to_mask(input_mask, **opts)
         delayed_extractor = self.parent._prep_for_extract(self.resolved_indexes)
-        return delayed_extractor.new(dtype, mask=mask, name=name)
+        return delayed_extractor.new(dtype, mask=mask, name=name, **opts)
 
     dup = new
 
@@ -331,7 +332,7 @@ def _extract_delayed(self):
         """Return an Expression object, treating this as an extract call"""
         return self.parent._prep_for_extract(self.resolved_indexes)
 
-    def _input_mask_to_mask(self, input_mask):
+    def _input_mask_to_mask(self, input_mask, **opts):
         from .vector import Vector
 
         if type(input_mask.parent) is Vector and type(self.parent) is not Vector:
@@ -374,7 +375,7 @@ def _input_mask_to_mask(self, input_mask):
             )
         else:
             mask_expr = input_mask.parent._prep_for_extract(self.resolved_indexes)
-        mask_value = mask_expr.new(name="mask_temp")
+        mask_value = mask_expr.new(name="mask_temp", **opts)
         return type(input_mask)(mask_value)
 
     def _repr_html_(self):
@@ -401,11 +402,12 @@ def dtype(self):
 
 
 class Updater:
-    __slots__ = "parent", "kwargs", "__weakref__"
+    __slots__ = "parent", "kwargs", "opts", "__weakref__"
 
-    def __init__(self, parent, **kwargs):
+    def __init__(self, parent, *, opts, **kwargs):
         self.parent = parent
         self.kwargs = kwargs
+        self.opts = opts
 
     def __getitem__(self, keys):
         # Occurs when user calls `C(params)[index]`
@@ -419,18 +421,26 @@ def _setitem(self, resolved_indexes, obj, *, is_submask):
         # Occurs when user calls C(params)[index] = expr
         if resolved_indexes.is_single_element and not self.kwargs:
             # Fast path using assignElement
+            if self.opts:
+                # Ignore opts for now
+                descriptor_lookup(**self.opts)
             self.parent._assign_element(resolved_indexes, obj)
         else:
             mask = self.kwargs.get("mask")
             replace = self.kwargs.get("replace", False)
             expr, alt_mask = self.parent._prep_for_assign(
-                resolved_indexes, obj, mask=mask, is_submask=is_submask, replace=replace
+                resolved_indexes,
+                obj,
+                mask,
+                is_submask,
+                replace,
+                self.opts,
             )
             if alt_mask is mask:
                 self.update(expr)
             else:
                 kwargs = dict(self.kwargs, mask=alt_mask)
-                Updater(self.parent, **kwargs).update(expr)
+                Updater(self.parent, opts=self.opts, **kwargs).update(expr)
 
     def __setitem__(self, keys, obj):
         if self.parent._is_scalar:
@@ -456,11 +466,11 @@ def __delitem__(self, keys):
 
     def __lshift__(self, expr):
         # Occurs when user calls `C(params) << expr`
-        self.parent._update(expr, **self.kwargs)
+        self.parent._update(expr, opts=self.opts, **self.kwargs)
 
     def update(self, expr):
         # Occurs when user calls `C(params).update(expr)`
-        self.parent._update(expr, **self.kwargs)
+        self.parent._update(expr, opts=self.opts, **self.kwargs)
 
     def __eq__(self, other):
         raise TypeError(f"__eq__ not defined for objects of type {type(self)}.")
@@ -478,7 +488,7 @@ def __init__(self, left, right):
         self.right = right
         self._value = None
 
-    def new(self, dtype=None, *, mask=None, name=None):
+    def new(self, dtype=None, *, mask=None, name=None, **opts):
         if (
             mask is None
             and self._value is not None
@@ -490,7 +500,7 @@ def new(self, dtype=None, *, mask=None, name=None):
             self._value = None
             return rv
         expr = self._to_expr()
-        return expr.new(dtype, mask=mask, name=name)
+        return expr.new(dtype, mask=mask, name=name, **opts)
 
     dup = new
 
diff --git a/graphblas/core/infix.py b/graphblas/core/infix.py
index 5eccdfa06..66f60baed 100644
--- a/graphblas/core/infix.py
+++ b/graphblas/core/infix.py
@@ -289,10 +289,10 @@ class ScalarMatMulExpr(InfixExprBase):
     _infix = "@"
     _is_scalar = True
 
-    def new(self, dtype=None, *, is_cscalar=None, name=None):
+    def new(self, dtype=None, *, is_cscalar=None, name=None, **opts):
         # Rely on the default operator for the method
         expr = getattr(self.left, self.method_name)(self.right)
-        return expr.new(dtype, is_cscalar=is_cscalar, name=name)
+        return expr.new(dtype, is_cscalar=is_cscalar, name=name, **opts)
 
     dup = new
 
diff --git a/graphblas/core/mask.py b/graphblas/core/mask.py
index f06d42a38..4d42b12f0 100644
--- a/graphblas/core/mask.py
+++ b/graphblas/core/mask.py
@@ -31,7 +31,7 @@ def _repr_html_(self):
     def _carg(self):
         return self.parent.gb_obj[0]
 
-    def new(self, dtype=None, *, complement=False, mask=None, name=None):
+    def new(self, dtype=None, *, complement=False, mask=None, name=None, **opts):
         """Return a new object with True values determined by the mask(s).
 
         By default, the result is True wherever the mask(s) would have been applied,
@@ -68,9 +68,9 @@ def new(self, dtype=None, *, complement=False, mask=None, name=None):
         if mask is None:
             val = type(self.parent)(dtype, *self.parent.shape, name=name)
             if complement:
-                val(~self) << True
+                val(~self, **opts) << True
             else:
-                val(self) << True
+                val(self, **opts) << True
             return val
 
         from .base import _check_mask
@@ -78,9 +78,9 @@ def new(self, dtype=None, *, complement=False, mask=None, name=None):
         mask = _check_mask(mask)
         d = _COMPLEMENT_MASKS if complement else _COMBINE_MASKS
         func = d[type(self), type(mask)]
-        return func(self, mask, dtype, name)
+        return func(self, mask, dtype, name, opts)
 
-    def __and__(self, other):
+    def __and__(self, other, **opts):
         """Return the intersection of two masks as a new mask.
 
         `new_mask = mask1 & mask2` is equivalent to the following:
@@ -99,14 +99,14 @@ def __and__(self, other):
         complement = self.complement or other.complement
         d = _COMPLEMENT_MASKS if complement else _COMBINE_MASKS
         func = d[type(self), type(other)]
-        val = func(self, other, bool, None)
+        val = func(self, other, bool, None, opts)
         if complement:
             return ComplementedStructuralMask(val)
         return StructuralMask(val)
 
     __rand__ = __and__
 
-    def __or__(self, other):
+    def __or__(self, other, **opts):
         """Return the union of two masks as a new mask.
 
         `new_mask = mask1 | mask2` is equivalent to the following:
@@ -123,7 +123,7 @@ def __or__(self, other):
 
         other = _check_mask(other)
         func = _MASK_OR[type(self), type(other)]
-        return func(self, other)
+        return func(self, other, opts)
 
     __ror__ = __or__
 
@@ -207,64 +207,64 @@ def _name_html(self):
 #    V: value
 #    CS: complemented structural
 #    CV: complemented value
-def _combine_S_S(m1, m2, dtype, name):
+def _combine_S_S(m1, m2, dtype, name, opts):
     """S-S"""
-    return pair(m1.parent & m2.parent).new(dtype, name=name)
+    return pair(m1.parent & m2.parent).new(dtype, name=name, **opts)
 
 
-def _combine_S_A(m1, m2, dtype, name):
+def _combine_S_A(m1, m2, dtype, name, opts):
     """S-S, S-V, S-CS, S-CV"""
-    return one(m1.parent).new(dtype, mask=m2, name=name)
+    return one(m1.parent).new(dtype, mask=m2, name=name, **opts)
 
 
-def _combine_A_S(m1, m2, dtype, name):
+def _combine_A_S(m1, m2, dtype, name, opts):
     """S-S, V-S, CS-S, CV-S"""
-    return one(m2.parent).new(dtype, mask=m1, name=name)
+    return one(m2.parent).new(dtype, mask=m1, name=name, **opts)
 
 
-def _combine_V_A(m1, m2, dtype, name):
+def _combine_V_A(m1, m2, dtype, name, opts):
     """V-S, V-V, V-CS, V-CV"""
     if isinstance(m2, ValueMask) and m1.parent._nvals > m2.parent._nvals:
         m1, m2 = m2, m1
-    val = valuene(m1.parent).new(dtype, mask=m2, name=name)
+    val = valuene(m1.parent).new(dtype, mask=m2, name=name, **opts)
     if dtype != BOOL or backend != "suitesparse" or not val.ss.is_iso:
-        val(val.S) << True
+        val(val.S, **opts) << True
     return val
 
 
-def _combine_A_V(m1, m2, dtype, name):
+def _combine_A_V(m1, m2, dtype, name, opts):
     """S-V, V-V, CS-V, CV-V"""
-    val = valuene(m2.parent).new(dtype, mask=m1, name=name)
+    val = valuene(m2.parent).new(dtype, mask=m1, name=name, **opts)
     if dtype != BOOL or backend != "suitesparse" or not val.ss.is_iso:
-        val(val.S) << True
+        val(val.S, **opts) << True
     return val
 
 
-def _combine_CS_CS(m1, m2, dtype, name):
+def _combine_CS_CS(m1, m2, dtype, name, opts):
     """CS-CS"""
-    val = pair(m1.parent | m2.parent).new(dtype, name=name)
-    val(~val.S, replace=True) << True
+    val = pair(m1.parent | m2.parent).new(dtype, name=name, **opts)
+    val(~val.S, replace=True, **opts) << True
     return val
 
 
-def _combine_CS_CV(m1, m2, dtype, name):
+def _combine_CS_CV(m1, m2, dtype, name, opts):
     """CS-CV"""
-    val = pair(one(m1.parent).new() | m2.parent).new(dtype, name=name)
-    val(~val.V, replace=True) << True
+    val = pair(one(m1.parent).new(**opts) | m2.parent).new(dtype, name=name, **opts)
+    val(~val.V, replace=True, **opts) << True
     return val
 
 
-def _combine_CV_CS(m1, m2, dtype, name):
+def _combine_CV_CS(m1, m2, dtype, name, opts):
     """CV-CS"""
-    val = pair(m1.parent | one(m2.parent).new()).new(dtype, name=name)
-    val(~val.V, replace=True) << True
+    val = pair(m1.parent | one(m2.parent).new(**opts)).new(dtype, name=name, **opts)
+    val(~val.V, replace=True, **opts) << True
     return val
 
 
-def _combine_CV_CV(m1, m2, dtype, name):
+def _combine_CV_CV(m1, m2, dtype, name, opts):
     """CV-CV"""
-    val = lor(m1.parent | m2.parent).new(dtype, name=name)
-    val(~val.V, replace=True) << True
+    val = lor(m1.parent | m2.parent).new(dtype, name=name, **opts)
+    val(~val.V, replace=True, **opts) << True
     return val
 
 
@@ -299,85 +299,87 @@ def _combine_CV_CV(m1, m2, dtype, name):
 
 
 # Recipes to return the *complement* of combining two masks
-def _complement_S_S(m1, m2, dtype, name):
+def _complement_S_S(m1, m2, dtype, name, opts):
     """S-S"""
-    val = pair(m1.parent & m2.parent).new(dtype, name=name)
-    val(~val.S, replace=True) << True
+    val = pair(m1.parent & m2.parent).new(dtype, name=name, **opts)
+    val(~val.S, replace=True, **opts) << True
     return val
 
 
-def _complement_S_A(m1, m2, dtype, name):
+def _complement_S_A(m1, m2, dtype, name, opts):
     """S-S, S-V, S-CS, S-CV"""
-    val = one(m1.parent).new(dtype, mask=m2, name=name)
-    val(~val.S, replace=True) << True
+    val = one(m1.parent).new(dtype, mask=m2, name=name, **opts)
+    val(~val.S, replace=True, **opts) << True
     return val
 
 
-def _complement_A_S(m1, m2, dtype, name):
+def _complement_A_S(m1, m2, dtype, name, opts):
     """S-S, V-S, CS-S, CV-S"""
-    val = one(m2.parent).new(dtype, mask=m1, name=name)
-    val(~val.S, replace=True) << True
+    val = one(m2.parent).new(dtype, mask=m1, name=name, **opts)
+    val(~val.S, replace=True, **opts) << True
     return val
 
 
-def _complement_V_V(m1, m2, dtype, name):
+def _complement_V_V(m1, m2, dtype, name, opts):
     """V-V"""
-    val = land(m1.parent & m2.parent).new(dtype, name=name)
-    val(~val.V, replace=True) << True
+    val = land(m1.parent & m2.parent).new(dtype, name=name, **opts)
+    val(~val.V, replace=True, **opts) << True
     return val
 
 
-def _complement_CS_CS(m1, m2, dtype, name):
+def _complement_CS_CS(m1, m2, dtype, name, opts):
     """CS-CS"""
-    return pair(one(m1.parent).new() | one(m2.parent).new()).new(dtype, name=name)
+    return pair(one(m1.parent).new(**opts) | one(m2.parent).new(**opts)).new(
+        dtype, name=name, **opts
+    )
 
 
-def _complement_CS_A(m1, m2, dtype, name):
+def _complement_CS_A(m1, m2, dtype, name, opts):
     """CS-S, CS-V, CS-CS, CS-CV"""
-    val = one(m1.parent).new(dtype, name=name)
-    val(~m2) << True
+    val = one(m1.parent).new(dtype, name=name, **opts)
+    val(~m2, **opts) << True
     return val
 
 
-def _complement_A_CS(m1, m2, dtype, name):
+def _complement_A_CS(m1, m2, dtype, name, opts):
     """S-CS, V-CS, CS-CS, CV-CS"""
-    val = one(m2.parent).new(dtype, name=name)
-    val(~m1) << True
+    val = one(m2.parent).new(dtype, name=name, **opts)
+    val(~m1, **opts) << True
     return val
 
 
-def _complement_CS_CV(m1, m2, dtype, name):
+def _complement_CS_CV(m1, m2, dtype, name, opts):
     """CS-CV"""
-    val = pair(one(m1.parent).new() | m2.parent).new(dtype, name=name)
-    val(val.V, replace=True) << True
+    val = pair(one(m1.parent).new(**opts) | m2.parent).new(dtype, name=name, **opts)
+    val(val.V, replace=True, **opts) << True
     return val
 
 
-def _complement_CV_CS(m1, m2, dtype, name):
+def _complement_CV_CS(m1, m2, dtype, name, opts):
     """CV-CS"""
-    val = pair(m1.parent | one(m2.parent).new()).new(dtype, name=name)
-    val(val.V, replace=True) << True
+    val = pair(m1.parent | one(m2.parent).new(**opts)).new(dtype, name=name, **opts)
+    val(val.V, replace=True, **opts) << True
     return val
 
 
-def _complement_CV_CV(m1, m2, dtype, name):
+def _complement_CV_CV(m1, m2, dtype, name, opts):
     """CV-CV"""
-    val = lor(m1.parent | m2.parent).new(dtype, name=name)
-    val(val.V, replace=True) << True
+    val = lor(m1.parent | m2.parent).new(dtype, name=name, **opts)
+    val(val.V, replace=True, **opts) << True
     return val
 
 
-def _complement_CV_A(m1, m2, dtype, name):
+def _complement_CV_A(m1, m2, dtype, name, opts):
     """CV-S, CV-V, CV-CS, CV-CV"""
-    val = one(m1.parent).new(dtype, mask=~m1, name=name)
-    val(~m2) << True
+    val = one(m1.parent).new(dtype, mask=~m1, name=name, **opts)
+    val(~m2, **opts) << True
     return val
 
 
-def _complement_A_CV(m1, m2, dtype, name):
+def _complement_A_CV(m1, m2, dtype, name, opts):
     """S-CV, V-CV, CS-CV, CV-CV"""
-    val = one(m2.parent).new(dtype, mask=~m2, name=name)
-    val(~m1) << True
+    val = one(m2.parent).new(dtype, mask=~m2, name=name, **opts)
+    val(~m1, **opts) << True
     return val
 
 
@@ -411,63 +413,71 @@ def _complement_A_CV(m1, m2, dtype, name):
 }
 
 
-def _combine_S_S_mask_or(m1, m2):
+def _combine_S_S_mask_or(m1, m2, opts):
     """S-S"""
-    val = monoid.any(one(m1.parent).new(bool) | one(m2.parent).new(bool)).new()
+    val = monoid.any(one(m1.parent).new(bool, **opts) | one(m2.parent).new(bool, **opts)).new(
+        **opts
+    )
     return StructuralMask(val)
 
 
-def _combine_S_SV_mask_or(m1, m2):
+def _combine_S_SV_mask_or(m1, m2, opts):
     """S-V"""
-    val = monoid.any(one(m1.parent).new(bool) | one(m2.parent).new(bool, mask=m2)).new()
+    val = monoid.any(
+        one(m1.parent).new(bool, **opts) | one(m2.parent).new(bool, mask=m2, **opts)
+    ).new(**opts)
     return StructuralMask(val)
 
 
-def _combine_SV_S_mask_or(m1, m2):
+def _combine_SV_S_mask_or(m1, m2, opts):
     """V-S"""
-    val = monoid.any(one(m1.parent).new(bool, mask=m1) | one(m2.parent).new(bool)).new()
+    val = monoid.any(
+        one(m1.parent).new(bool, mask=m1, **opts) | one(m2.parent).new(bool, **opts)
+    ).new(**opts)
     return StructuralMask(val)
 
 
-def _complement_A_CS_mask_or(m1, m2):
+def _complement_A_CS_mask_or(m1, m2, opts):
     """~S-CS, ~V-CS, ~CV-CS"""
-    val = one(m2.parent).new(bool, mask=~m1)
+    val = one(m2.parent).new(bool, mask=~m1, **opts)
     return ComplementedStructuralMask(val)
 
 
-def _complement_CS_A_mask_or(m1, m2):
+def _complement_CS_A_mask_or(m1, m2, opts):
     """~CS-S, ~CS-V, ~CS-CV"""
-    val = one(m1.parent).new(bool, mask=~m2)
+    val = one(m1.parent).new(bool, mask=~m2, **opts)
     return ComplementedStructuralMask(val)
 
 
-def _complement_A_CV_mask_or(m1, m2):
+def _complement_A_CV_mask_or(m1, m2, opts):
     """~S-CV, ~V-CV"""
-    val = valuene(m2.parent).new(bool, mask=~m1)
+    val = valuene(m2.parent).new(bool, mask=~m1, **opts)
     return ComplementedStructuralMask(val)
 
 
-def _complement_CV_A_mask_or(m1, m2):
+def _complement_CV_A_mask_or(m1, m2, opts):
     """~CV-S, ~CV-V"""
-    val = valuene(m1.parent).new(bool, mask=~m2)
+    val = valuene(m1.parent).new(bool, mask=~m2, **opts)
     return ComplementedStructuralMask(val)
 
 
-def _combine_V_V_mask_or(m1, m2):
+def _combine_V_V_mask_or(m1, m2, opts):
     """V-V"""
-    val = monoid.any(valuene(m1.parent).new() | valuene(m2.parent).new()).new(bool)
+    val = monoid.any(valuene(m1.parent).new(**opts) | valuene(m2.parent).new(**opts)).new(
+        bool, **opts
+    )
     return StructuralMask(val)
 
 
-def _complement_CS_CS_mask_or(m1, m2):
+def _complement_CS_CS_mask_or(m1, m2, opts):
     """~CS-CS"""
-    val = pair(m1.parent & m2.parent).new(bool)
+    val = pair(m1.parent & m2.parent).new(bool, **opts)
     return ComplementedStructuralMask(val)
 
 
-def _complement_CV_CV_mask_or(m1, m2):
+def _complement_CV_CV_mask_or(m1, m2, opts):
     """~CV-CV"""
-    val = valuene(land(m1.parent & m2.parent).new(bool)).new()
+    val = valuene(land(m1.parent & m2.parent).new(bool, **opts)).new(**opts)
     return ComplementedStructuralMask(val)
 
 
diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py
index b0bf124b3..86ce74f30 100644
--- a/graphblas/core/matrix.py
+++ b/graphblas/core/matrix.py
@@ -9,6 +9,7 @@
 from ..exceptions import DimensionMismatch, InvalidValue, NoValue, check_status
 from . import automethods, ffi, lib, utils
 from .base import BaseExpression, BaseType, _check_mask, call
+from .descriptor import lookup as descriptor_lookup
 from .expr import _ALL_INDICES, AmbiguousAssignOrExtract, IndexerResolver, Updater
 from .mask import Mask, StructuralMask, ValueMask
 from .operator import UNKNOWN_OPCLASS, find_opclass, get_semiring, get_typed_op, op_from_string
@@ -53,8 +54,10 @@
 # Custom recipes
 def _m_add_v(updater, left, right, op):
     full = Vector(right.dtype, left._nrows, name="v_full")
-    full[:] = 0
-    temp = full.outer(right, binary.second).new(name="M_temp", mask=updater.kwargs.get("mask"))
+    full(**updater.opts)[:] = 0
+    temp = full.outer(right, binary.second).new(
+        name="M_temp", mask=updater.kwargs.get("mask"), **updater.opts
+    )
     updater << left.ewise_add(temp, op)
 
 
@@ -64,19 +67,22 @@ def _m_mult_v(updater, left, right, op):
 
 def _m_union_m(updater, left, right, left_default, right_default, op, dtype):
     mask = updater.kwargs.get("mask")
+    opts = updater.opts
     new_left = left.dup(dtype, clear=True)
-    new_left(mask=mask) << binary.second(right, left_default)
-    new_left(mask=mask) << binary.first(left | new_left)
+    new_left(mask=mask, **opts) << binary.second(right, left_default)
+    new_left(mask=mask, **opts) << binary.first(left | new_left)
     new_right = right.dup(dtype, clear=True)
-    new_right(mask=mask) << binary.second(left, right_default)
-    new_right(mask=mask) << binary.first(right | new_right)
+    new_right(mask=mask, **opts) << binary.second(left, right_default)
+    new_right(mask=mask, **opts) << binary.first(right | new_right)
     updater << op(new_left & new_right)
 
 
 def _m_union_v(updater, left, right, left_default, right_default, op):
     full = Vector(right.dtype, left._nrows, name="v_full")
-    full[:] = 0
-    temp = full.outer(right, binary.second).new(name="M_temp", mask=updater.kwargs.get("mask"))
+    full(**updater.opts)[:] = 0
+    temp = full.outer(right, binary.second).new(
+        name="M_temp", mask=updater.kwargs.get("mask"), **updater.opts
+    )
     updater << left.ewise_union(temp, op, left_default=left_default, right_default=right_default)
 
 
@@ -212,7 +218,7 @@ def V(self):
         """Create a Mask based on the values in the Matrix (treating each value as truthy)."""
         return ValueMask(self)
 
-    def __delitem__(self, keys):
+    def __delitem__(self, keys, **opts):
         """Delete a single element, row/column, or submatrix.
 
         Examples
@@ -220,7 +226,7 @@ def __delitem__(self, keys):
 
             >>> del M[1, 5]
         """
-        del Updater(self)[keys]
+        del Updater(self, opts=opts)[keys]
 
     def __getitem__(self, keys):
         """Extract a single element, row/column, or submatrix.
@@ -243,7 +249,7 @@ def __getitem__(self, keys):
             return VectorIndexExpr(self, resolved_indexes, *shape)
         return MatrixIndexExpr(self, resolved_indexes, *shape)
 
-    def __setitem__(self, keys, expr):
+    def __setitem__(self, keys, expr, **opts):
         """Assign values to a single element, row/column, or submatrix.
 
         See the `Assign section <../user_guide/operations.html#assign>`__
@@ -256,7 +262,7 @@ def __setitem__(self, keys, expr):
 
             M[0, 0:3] = 17
         """
-        Updater(self)[keys] = expr
+        Updater(self, opts=opts)[keys] = expr
 
     def __contains__(self, index):
         """Indicates whether the (row, col) index has a value present.
@@ -289,7 +295,7 @@ def __sizeof__(self):
             return size[0] + object.__sizeof__(self)
         raise TypeError("Unable to get size of Matrix with backend: {backend}")
 
-    def isequal(self, other, *, check_dtype=False):
+    def isequal(self, other, *, check_dtype=False, **opts):
         """Check for exact equality (same size, same structure).
 
         Parameters
@@ -324,15 +330,15 @@ def isequal(self, other, *, check_dtype=False):
             op = get_typed_op(binary.eq, self.dtype, other.dtype, kind="binary")
 
         matches = Matrix(bool, self._nrows, self._ncols, name="M_isequal")
-        matches << self.ewise_mult(other, op)
+        matches(**opts) << self.ewise_mult(other, op)
         # ewise_mult performs intersection, so nvals will indicate mismatched empty values
         if matches._nvals != self._nvals:
             return False
 
         # Check if all results are True
-        return matches.reduce_scalar(monoid.land, allow_empty=False).new().value
+        return matches.reduce_scalar(monoid.land, allow_empty=False).new(**opts).value
 
-    def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False):
+    def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False, **opts):
         """Check for approximate equality (including same size and same structure).
 
         Equivalent to: ``abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)``.
@@ -366,14 +372,14 @@ def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False):
             return False
 
         matches = self.ewise_mult(other, binary.isclose(rel_tol, abs_tol)).new(
-            bool, name="M_isclose"
+            bool, name="M_isclose", **opts
         )
         # ewise_mult performs intersection, so nvals will indicate mismatched empty values
         if matches._nvals != self._nvals:
             return False
 
         # Check if all results are True
-        return matches.reduce_scalar(monoid.land, allow_empty=False).new().value
+        return matches.reduce_scalar(monoid.land, allow_empty=False).new(**opts).value
 
     @property
     def nrows(self):
@@ -595,7 +601,7 @@ def build(self, rows, columns, values, *, dup_op=None, clear=False, nrows=None,
         if not dup_op_given and self._nvals < n:
             raise ValueError("Duplicate indices found, must provide `dup_op` BinaryOp")
 
-    def dup(self, dtype=None, *, clear=False, mask=None, name=None):
+    def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts):
         """Create a duplicate of the Matrix.
 
         This is a full copy, not a view on the original.
@@ -621,14 +627,17 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None):
                 dtype = self.dtype
             rv = Matrix(dtype, nrows=self._nrows, ncols=self._ncols, name=name)
             if not clear:
-                rv(mask=mask)[...] = self
+                rv(mask=mask, **opts)[...] = self
         else:
+            if opts:
+                # Ignore opts for now
+                descriptor_lookup(**opts)
             new_mat = ffi_new("GrB_Matrix*")
             rv = Matrix._from_obj(new_mat, self.dtype, self._nrows, self._ncols, name=name)
             call("GrB_Matrix_dup", [_Pointer(rv), self])
         return rv
 
-    def diag(self, k=0, dtype=None, *, name=None):
+    def diag(self, k=0, dtype=None, *, name=None, **opts):
         """Return a Vector built from the diagonal values of the Matrix.
 
         Parameters
@@ -654,7 +663,7 @@ def diag(self, k=0, dtype=None, *, name=None):
             dtype = self.dtype
         if type(k) is not Scalar:
             k = Scalar.from_value(k, INT64, is_cscalar=True, name="")
-        D = select.diag(self, k).new(dtype, name="Diag_temp")
+        D = select.diag(self, k).new(dtype, name="Diag_temp", **opts)
         k = k.value
         if k < 0:
             size = min(self._nrows + k, self._ncols)
@@ -664,13 +673,13 @@ def diag(self, k=0, dtype=None, *, name=None):
             size = 0
         rv = Vector(dtype, size=size, name=name)
         if k == 0:
-            rv << D.reduce_rowwise(monoid.any)
+            rv(**opts) << D.reduce_rowwise(monoid.any)
         else:
-            d = D.reduce_rowwise(monoid.any).new(name="diag_temp")
+            d = D.reduce_rowwise(monoid.any).new(name="diag_temp", **opts)
             if k < 0:
-                rv << d[d._size - rv._size :]
+                rv(**opts) << d[d._size - rv._size :]
             else:
-                rv << d[: rv._size]
+                rv(**opts) << d[: rv._size]
         return rv
 
     def wait(self, how="materialize"):
@@ -2412,7 +2421,9 @@ def reposition(self, row_offset, column_offset, *, nrows=None, ncols=None):
     ##################################
     # Extract and Assign index methods
     ##################################
-    def _extract_element(self, resolved_indexes, dtype=None, *, is_cscalar, name=None, result=None):
+    def _extract_element(
+        self, resolved_indexes, dtype, opts, *, is_cscalar, name=None, result=None
+    ):
         if dtype is None:
             dtype = self.dtype
         else:
@@ -2422,6 +2433,9 @@ def _extract_element(self, resolved_indexes, dtype=None, *, is_cscalar, name=Non
             rowidx, colidx = colidx, rowidx
         if result is None:
             result = Scalar(dtype, is_cscalar=is_cscalar, name=name)
+        if opts:
+            # Ignore opts for now
+            descriptor_lookup(**opts)
         if is_cscalar:
             dtype_name = "UDT" if dtype._is_udt else dtype.name
             if (
@@ -2501,7 +2515,7 @@ def _assign_element(self, resolved_indexes, value):
             cfunc_name = "GrB_Matrix_setElement_Scalar"
         call(cfunc_name, [self, value, rowidx.index, colidx.index])
 
-    def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False, replace=False):
+    def _prep_for_assign(self, resolved_indexes, value, mask, is_submask, replace, opts):
         method_name = "__setitem__"
         rowidx, colidx = resolved_indexes.indices
         rowsize = rowidx.size
@@ -2558,7 +2572,9 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                         else:
                             # Recipe: create a new mask by expanding the old mask
                             cfunc_name = "GrB_Row_assign"
-                            mask = _vanilla_subassign_mask(self, mask, rowidx, colidx, replace)
+                            mask = _vanilla_subassign_mask(
+                                self, mask, rowidx, colidx, replace, opts
+                            )
                     else:
                         # C(m)[i, J] << v
                         # C[i, J] << v
@@ -2606,7 +2622,9 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                         else:
                             # Recipe: create a new mask by expanding the old mask
                             cfunc_name = "GrB_Col_assign"
-                            mask = _vanilla_subassign_mask(self, mask, rowidx, colidx, replace)
+                            mask = _vanilla_subassign_mask(
+                                self, mask, rowidx, colidx, replace, opts
+                            )
                     else:
                         # C(m)[I, j] << v
                         # C[I, j] << v
@@ -2671,7 +2689,7 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                     cfunc_name = "GxB_Matrix_subassign"
                 else:
                     cfunc_name = "GrB_Matrix_assign"
-                    mask = _vanilla_subassign_mask(self, mask, rowidx, colidx, replace)
+                    mask = _vanilla_subassign_mask(self, mask, rowidx, colidx, replace, opts)
             else:
                 # C[I, J] << A
                 # C(M)[I, J] << A
@@ -2759,9 +2777,10 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                             return self._prep_for_assign(
                                 resolved_indexes,
                                 vals,
-                                mask=mask,
-                                is_submask=is_submask,
-                                replace=replace,
+                                mask,
+                                is_submask,
+                                replace,
+                                opts,
                             )
                     if rowsize is None or colsize is None:
                         types = (Scalar, Vector)
@@ -2779,7 +2798,7 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                     if is_submask:
                         # C[i, J](m) << c
                         value_vector = Vector(value.dtype, size=mask.parent._size, name="v_temp")
-                        value_vector << value
+                        value_vector(**opts) << value
                         expr_repr = (
                             "[{1._expr_name}, [{3._expr_name} cols]](%s) << {0.name}" % mask.name
                         )
@@ -2787,14 +2806,16 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                             cfunc_name = "GxB_Row_subassign"
                         else:
                             cfunc_name = "GrB_Row_assign"
-                            mask = _vanilla_subassign_mask(self, mask, rowidx, colidx, replace)
+                            mask = _vanilla_subassign_mask(
+                                self, mask, rowidx, colidx, replace, opts
+                            )
                     else:
                         # C(m)[i, J] << c
                         # C[i, J] << c
                         cfunc_name = "GrB_Row_assign"
                         value_vector = Vector(value.dtype, size=colsize, name="v_temp")
                         expr_repr = "[{1._expr_name}, [{3._expr_name} cols]] = {0.name}"
-                        value_vector << value
+                        value_vector(**opts) << value
 
                     # Row-only selection
                     expr = MatrixExpression(
@@ -2810,18 +2831,20 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                     if is_submask:
                         # C[I, j](m) << c
                         value_vector = Vector(value.dtype, size=mask.parent._size, name="v_temp")
-                        value_vector << value
+                        value_vector(**opts) << value
                         if backend == "suitesparse":
                             cfunc_name = "GxB_Col_subassign"
                         else:
                             cfunc_name = "GrB_Col_assign"
-                            mask = _vanilla_subassign_mask(self, mask, rowidx, colidx, replace)
+                            mask = _vanilla_subassign_mask(
+                                self, mask, rowidx, colidx, replace, opts
+                            )
                     else:
                         # C(m)[I, j] << c
                         # C[I, j] << c
                         cfunc_name = "GrB_Col_assign"
                         value_vector = Vector(value.dtype, size=rowsize, name="v_temp")
-                        value_vector << value
+                        value_vector(**opts) << value
 
                     # Column-only selection
                     expr = MatrixExpression(
@@ -2868,13 +2891,17 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                             cfunc_name = f"GxB_Matrix_subassign_{dtype_name}"
                         else:
                             cfunc_name = f"GrB_Matrix_assign_{dtype_name}"
-                            mask = _vanilla_subassign_mask(self, mask, rowidx, colidx, replace)
+                            mask = _vanilla_subassign_mask(
+                                self, mask, rowidx, colidx, replace, opts
+                            )
                     else:
                         if backend == "suitesparse":
                             cfunc_name = "GxB_Matrix_subassign_Scalar"
                         else:
                             cfunc_name = "GrB_Matrix_assign_Scalar"
-                            mask = _vanilla_subassign_mask(self, mask, rowidx, colidx, replace)
+                            mask = _vanilla_subassign_mask(
+                                self, mask, rowidx, colidx, replace, opts
+                            )
                     expr_repr = (
                         "[[{2._expr_name} rows], [{4._expr_name} cols]](%s) = {0._expr_name}"
                         % mask.name
@@ -2924,7 +2951,7 @@ def _delete_element(self, resolved_indexes):
     )
 
 
-def _vanilla_subassign_mask(self, mask, rows, cols, replace):
+def _vanilla_subassign_mask(self, mask, rows, cols, replace, opts):
     is_rowwise = rows.size is None and cols.size is not None
     is_colwise = rows.size is not None and cols.size is None
     is_matrix = not is_rowwise and not is_colwise
@@ -2934,33 +2961,33 @@ def _vanilla_subassign_mask(self, mask, rows, cols, replace):
         rows = rows._py_index()
         cols = cols._py_index()
         val = Matrix(mask.parent.dtype, nrows=self._nrows, ncols=self._ncols, name="M_temp")
-        val[rows, cols] = mask.parent
+        val(**opts)[rows, cols] = mask.parent
     elif is_rowwise:
         if not replace and cols.index is _ALL_INDICES:
             return mask
         cols = cols._py_index()
         val = Vector(mask.parent.dtype, size=self._ncols, name="m_temp")
-        val[cols] = mask.parent
+        val(**opts)[cols] = mask.parent
     elif is_colwise:
         if not replace and rows.index is _ALL_INDICES:
             return mask
         rows = rows._py_index()
         val = Vector(mask.parent.dtype, size=self._nrows, name="m_temp")
-        val[rows] = mask.parent
+        val(**opts)[rows] = mask.parent
     else:  # pragma: no cover (sanity)
         raise RuntimeError("oops! Submask does not work on scalars")
     mask = type(mask)(val)
     if replace:
         if is_matrix:
             val = self.dup()
-            del val[rows, cols]
+            val.__delitem__((rows, cols), **opts)  # del val[rows, cols]
         elif is_rowwise:
-            val = self[rows.index, :].new()
-            del val[cols]
+            val = self[rows.index, :].new(**opts)
+            val.__delitem__(cols, **opts)  # del val[cols]
         else:  # is_colwise:
-            val = self[:, cols.index].new()
-            del val[rows]
-        mask |= val.S
+            val = self[:, cols.index].new(**opts)
+            val.__delitem__(rows, **opts)  # del val[rows]
+        mask = mask.__or__(val.S, **opts)  # mask |= val.S
     return mask
 
 
@@ -3205,22 +3232,19 @@ def _repr_html_(self, collapse=False):
 
         return format_matrix_html(self, collapse=collapse)
 
-    def new(self, dtype=None, *, mask=None, name=None):
+    def new(self, dtype=None, *, mask=None, name=None, **opts):
         if dtype is None:
             dtype = self.dtype
         output = Matrix(dtype, self._nrows, self._ncols, name=name)
-        if mask is None:
-            output.update(self)
-        else:
-            output(mask=mask).update(self)
+        output(mask=mask, **opts) << self
         return output
 
-    def dup(self, dtype=None, *, clear=False, mask=None, name=None):
+    def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts):
         if dtype is None:
             dtype = self.dtype
         if clear:
             return Matrix(dtype, self._nrows, self._ncols, name=name)
-        return self.new(dtype, mask=mask, name=name)
+        return self.new(dtype, mask=mask, name=name, **opts)
 
     @property
     def T(self):
@@ -3249,8 +3273,8 @@ def to_values(self, dtype=None, *, rows=True, columns=True, values=True, sort=Tr
         return cols, rows, vals
 
     @wrapdoc(Matrix.diag)
-    def diag(self, k=0, dtype=None, *, name=None):
-        return self._matrix.diag(-k, dtype, name=name)
+    def diag(self, k=0, dtype=None, *, name=None, **opts):
+        return self._matrix.diag(-k, dtype, name=name, **opts)
 
     @property
     def _carg(self):
diff --git a/graphblas/core/scalar.py b/graphblas/core/scalar.py
index 30c303310..9c0b53504 100644
--- a/graphblas/core/scalar.py
+++ b/graphblas/core/scalar.py
@@ -609,10 +609,10 @@ def construct_output(self, dtype=None, *, is_cscalar=None, name=None):
             is_cscalar = self._is_cscalar
         return Scalar(dtype, is_cscalar=is_cscalar, name=name)
 
-    def new(self, dtype=None, *, is_cscalar=None, name=None):
+    def new(self, dtype=None, *, is_cscalar=None, name=None, **opts):
         if is_cscalar is None:
             is_cscalar = self._is_cscalar
-        return super()._new(dtype, None, name, is_cscalar=is_cscalar)
+        return super()._new(dtype, None, name, is_cscalar=is_cscalar, **opts)
 
     dup = new
 
@@ -672,11 +672,11 @@ class ScalarIndexExpr(AmbiguousAssignOrExtract):
     _is_scalar = True
     _is_cscalar = False
 
-    def new(self, dtype=None, *, is_cscalar=None, name=None):
+    def new(self, dtype=None, *, is_cscalar=None, name=None, **opts):
         if is_cscalar is None:
             is_cscalar = False
         return self.parent._extract_element(
-            self.resolved_indexes, dtype, is_cscalar=is_cscalar, name=name
+            self.resolved_indexes, dtype, opts, is_cscalar=is_cscalar, name=name
         )
 
     dup = new
diff --git a/graphblas/core/ss/config.py b/graphblas/core/ss/config.py
index db2605ca1..6e0a999f8 100644
--- a/graphblas/core/ss/config.py
+++ b/graphblas/core/ss/config.py
@@ -4,12 +4,13 @@
 from suitesparse_graphblas import vararg
 
 from ...dtypes import lookup_dtype
-from ...exceptions import _error_code_lookup
+from ...exceptions import _error_code_lookup, check_status
 from .. import NULL, ffi, lib
 from ..utils import values_to_numpy_buffer
 
 
 class BaseConfig(MutableMapping):
+    _initialized = False
     # Subclasses should redefine these
     _get_function = None
     _set_function = None
@@ -22,16 +23,32 @@ class BaseConfig(MutableMapping):
     _read_only = set()
     _set_ctypes = {
         "GxB_Format_Value": "int",
+        "GrB_Desc_Value": "int",
         "bool": "int",
     }
 
     def __init__(self, parent=None):
-        for d in self._enumerations.values():
-            for k, v in list(d.items()):
-                d[v] = k
-        for d in self._bitwise.values():
-            for k, v in list(d.items()):
-                d[v] = k
+        cls = type(self)
+        if not cls._initialized:
+            cls._reverse_enumerations = {}
+            for key, d in self._enumerations.items():
+                cls._reverse_enumerations[key] = rd = {}
+                for k, v in list(d.items()):
+                    if v not in d:
+                        d[v] = v
+                    rd[v] = k
+                    if k not in rd:
+                        rd[k] = k
+            cls._reverse_bitwise = {}
+            for key, d in self._bitwise.items():
+                cls._reverse_bitwise[key] = rd = {}
+                for k, v in list(d.items()):
+                    if v not in d:  # pragma: no branch (safety)
+                        d[v] = v
+                    rd[v] = k
+                    if k not in rd:  # pragma: no branch (safety)
+                        rd[k] = k
+            cls._initialized = True
         self._parent = parent
 
     def __delitem__(self, key):
@@ -51,15 +68,14 @@ def __getitem__(self, key):
         if info == lib.GrB_SUCCESS:  # pragma: no branch (safety)
             if is_array:
                 return list(val_ptr)
-            if key in self._enumerations:
-                return self._enumerations[key][val_ptr[0]]
-            if key in self._bitwise:
-                bitwise = self._bitwise[key]
+            if key in self._reverse_enumerations:
+                return self._reverse_enumerations[key][val_ptr[0]]
+            if key in self._reverse_bitwise:
                 val = val_ptr[0]
-                if val in bitwise:
-                    return {bitwise[val]}
+                if val in (reverse_bitwise := self._reverse_bitwise[key]):
+                    return {reverse_bitwise[val]}
                 rv = set()
-                for k, v in bitwise.items():
+                for k, v in self._bitwise[key].items():
                     if isinstance(k, str) and val & v and bin(v).count("1") == 1:
                         rv.add(k)
                 return rv
@@ -74,13 +90,20 @@ def __setitem__(self, key, val):
             raise ValueError(f"Config option {key!r} is read-only")
         key_obj, ctype = self._options[key]
         ctype = self._set_ctypes.get(ctype, ctype)
-        if key in self._enumerations and isinstance(val, str):
-            val = val.lower()
-            val = self._enumerations[key][val]
-        elif key in self._bitwise and val is not None and not isinstance(val, Integral):
+        if val is None:
+            pass
+        elif key in self._enumerations:
+            if isinstance(val, str):
+                val = val.lower()
+                val = self._enumerations[key][val]
+            else:
+                val = self._enumerations[key].get(val, val)
+        elif key in self._bitwise:
             bitwise = self._bitwise[key]
             if isinstance(val, str):
                 val = bitwise[val.lower()]
+            elif isinstance(val, Integral):
+                val = bitwise.get(val, val)
             else:
                 bits = 0
                 for x in val:
@@ -90,10 +113,9 @@ def __setitem__(self, key, val):
                         bits |= x
                 val = bits
         if val is None:
-            if key in self._defaults:
-                val = self._defaults[key]
-            else:
+            if key not in self._defaults:
                 raise ValueError(f"Unable to set default value for {key!r}")
+            val = self._defaults[key]
         if val is None:
             val_obj = NULL
         elif "[" in ctype:
@@ -114,7 +136,11 @@ def __setitem__(self, key, val):
         else:
             info = self._set_function(self._parent._carg, key_obj, vararg(val_obj))
         if info != lib.GrB_SUCCESS:
-            raise _error_code_lookup[info](f"Failed to set info for {key!r}")
+            if self._parent is not None:  # pragma: no branch (safety)
+                check_status(info, self._parent)
+            raise _error_code_lookup[info](  # pragma: no cover (safety)
+                f"Failed to set info for {key!r}"
+            )
 
     def __iter__(self):
         return iter(sorted(self._options))
diff --git a/graphblas/core/ss/descriptor.py b/graphblas/core/ss/descriptor.py
index e85d4f6fd..cb199716c 100644
--- a/graphblas/core/ss/descriptor.py
+++ b/graphblas/core/ss/descriptor.py
@@ -1,10 +1,13 @@
 from suitesparse_graphblas import vararg
 
-from ...exceptions import check_status
+from ...exceptions import check_status, check_status_carg
 from .. import ffi, lib
-from ..descriptor import Descriptor, _desc_map
+from ..descriptor import Descriptor
+from .config import BaseConfig
 
-str_to_compression = {
+ffi_new = ffi.new
+
+_str_to_compression = {
     "none": lib.GxB_COMPRESSION_NONE,
     "default": lib.GxB_COMPRESSION_DEFAULT,
     "lz4": lib.GxB_COMPRESSION_LZ4,
@@ -13,40 +16,169 @@
 }
 
 
-def set_nthreads(desc, nthreads):
-    check_status(
-        lib.GxB_Desc_set(
-            desc._carg,
-            lib.GxB_NTHREADS,
-            vararg(ffi.cast("int", nthreads)),
-        ),
-        desc,
-    )
+# It would be great if we could make Descriptor class more like this
+class _DescriptorConfig(BaseConfig):
+    _get_function = lib.GxB_Desc_get
+    _set_function = lib.GxB_Desc_set
+    _options = {
+        # GrB
+        "output_replace": (lib.GrB_OUTP, "GrB_Desc_Value"),
+        "mask_complement": (lib.GrB_MASK, "GrB_Desc_Value"),
+        "mask_structure": (lib.GrB_MASK, "GrB_Desc_Value"),
+        "transpose_first": (lib.GrB_INP0, "GrB_Desc_Value"),
+        "transpose_second": (lib.GrB_INP1, "GrB_Desc_Value"),
+        # GxB
+        "nthreads": (lib.GxB_DESCRIPTOR_NTHREADS, "int"),
+        "chunk": (lib.GxB_DESCRIPTOR_CHUNK, "double"),
+        "axb_method": (lib.GxB_AxB_METHOD, "GrB_Desc_Value"),
+        "sort": (lib.GxB_SORT, "int"),
+        "secure_import": (lib.GxB_IMPORT, "int"),
+        # "gpu_control": (GxB_DESCRIPTOR_GPU_CONTROL, "GrB_Desc_Value"),  # Coming soon...
+    }
+    _enumerations = {
+        # GrB
+        "output_replace": {
+            True: lib.GrB_REPLACE,
+            False: False,
+        },
+        "mask_complement": {
+            True: lib.GrB_COMP,
+            False: False,
+        },
+        "mask_structure": {
+            True: lib.GrB_STRUCTURE,
+            False: False,
+        },
+        "transpose_first": {
+            True: lib.GrB_TRAN,
+            False: False,
+        },
+        "transpose_second": {
+            True: lib.GrB_TRAN,
+            False: False,
+        },
+        # GxB
+        "axb_method": {
+            "gustavson": lib.GxB_AxB_GUSTAVSON,
+            "dot": lib.GxB_AxB_DOT,
+            "hash": lib.GxB_AxB_HASH,
+            "saxpy": lib.GxB_AxB_SAXPY,
+            "default": lib.GxB_DEFAULT,
+        },
+        "secure_import": {
+            True: lib.GxB_SECURE_IMPORT,
+            False: False,
+        },
+        "sort": {
+            False: False,
+            True: lib.GxB_SORT,
+        },
+        # "gpu_control": {  # Coming soon...
+        #     "always": lib.GxB_GPU_ALWAYS,
+        #     "never": lib.GxB_GPU_NEVER,
+        # },
+    }
+    _defaults = {
+        # GrB
+        "output_replace": False,
+        "mask_complement": False,
+        "mask_structure": False,
+        "transpose_first": False,
+        "transpose_second": False,
+        # GxB
+        "nthreads": 0,
+        "chunk": 0,
+        "axb_method": "default",
+        "sort": False,
+        "secure_import": False,
+    }
+    _count = 0
+
+    def __init__(self):
+        gb_obj = ffi_new("GrB_Descriptor*")
+        check_status_carg(lib.GrB_Descriptor_new(gb_obj), "Descriptor", gb_obj[0])
+        parent = Descriptor(gb_obj)
+        initialized = self._initialized
+        super().__init__(parent)
+        if not initialized:
+            # These are actually bitwise, but we treat them as boolean, add extra mappings for get
+            self._enumerations["mask_complement"][lib.GrB_COMP | lib.GrB_STRUCTURE] = lib.GrB_COMP
+            self._enumerations["mask_structure"][
+                lib.GrB_COMP | lib.GrB_STRUCTURE
+            ] = lib.GrB_STRUCTURE
+            self._enumerations["mask_complement"][lib.GrB_STRUCTURE] = False
+            self._enumerations["mask_structure"][lib.GrB_COMP] = False
 
 
-def get_nthreads_descriptor(nthreads, _cache=True):
-    nthreads = max(0, int(nthreads))
-    key = ("nthreads", nthreads)
-    if _cache and key in _desc_map:
-        return _desc_map[key]
-    desc_obj = ffi.new("GrB_Descriptor*")
-    lib.GrB_Descriptor_new(desc_obj)
-    desc = Descriptor(desc_obj[0], f"nthreads{nthreads}")
-    set_nthreads(desc, nthreads)
-    if _cache:
-        _desc_map[key] = desc
+def get_descriptor(**opts):
+    """Create descriptor with SuiteSparse:GraphBLAS options
+
+    See SuiteSparse:GraphBLAS documentation for more details.
+
+    Default descriptor parameters
+    -----------------------------
+    output_replace : bool, default False
+    mask_complement : bool, default False
+    mask_structure : bool, default False
+    transpose_first : bool, default False
+    transpose_second : bool: default False
+
+    Extended descriptor parameters
+    ------------------------------
+    nthreads : int
+        Maximum number of OpenMP threads to use. 0 or negative means no limit.
+        If not set, use nthreads from global config ``gb.ss.config["nthreads"]``
+    chunk : double
+    axb_method : str, {"gustavson", "dot", "hash", "saxpy", "default"}
+        A hint for which matrix multiply method to use
+    sort : bool, default False
+        A hint for whether methods may return a "jumbled" matrix
+    secure_import : bool, default False
+        Whether to trust the data for `import` and `pack` functions.
+        When True, checks are performed to ensure input data is valid.
+    compression : str, {"none", "default", "lz4", "lz4hc", "zstd"}
+        Whether and how to compress the data for serialization.
+        The default is "zstd" with ``compression_level=1``
+    compression_level : int
+        1-9 for "lz4hc" compression: (1) is the fastest, (9) is the most compact (default 1).
+        1-19 for "zstd" compression: (1) is the fastest, (19) is the most compact (default 1).
+        Ignored for other compression methods.
+
+    Returns
+    -------
+    Descriptor or None
+    """
+    if not opts or all(val is False or val is None for val in opts.values()):
+        return None
+    config = _DescriptorConfig()
+    desc = config._parent
+    if "compression" in opts or "compression_level" in opts:
+        compression = opts.pop("compression", None)
+        level = opts.pop("compression_level", None)
+        _set_compression(desc, compression, level)
+    if (nthreads := opts.pop("nthreads", None)) is not None:
+        config["nthreads"] = max(0, nthreads)
+    for key, val in opts.items():
+        if val is False or val is None:
+            continue
+        try:
+            config[key] = val
+        except KeyError:
+            raise ValueError(
+                f"Descriptor option {key!r} not understood with suitesparse backend"
+            ) from None
     return desc
 
 
-def get_compression_descriptor(compression="default", level=None, nthreads=None):
+def _set_compression(desc, compression, level):
     if compression is None:
-        comp = str_to_compression["none"]
-    elif not isinstance(compression, str) or compression.lower() not in str_to_compression:
-        valid = ", ".join(sorted(map(repr, str_to_compression)))
+        comp = _str_to_compression["none"]
+    elif not isinstance(compression, str) or compression.lower() not in _str_to_compression:
+        valid = ", ".join(sorted(map(repr, _str_to_compression)))
         raise ValueError(f"compression argument should be one of {valid}")
     else:
         compression = compression.lower()
-        comp = str_to_compression[compression]
+        comp = _str_to_compression[compression]
     if level is not None:
         if compression not in {"lz4hc", "zstd"}:
             raise TypeError('level argument is only valid when using "lz4hc" compression')
@@ -59,23 +191,6 @@ def get_compression_descriptor(compression="default", level=None, nthreads=None)
                 f"1 is the fastest, {upper} is the most compression (default is {default})."
             )
         comp += level
-    if nthreads is not None:
-        nthreads = max(0, int(nthreads))
-        key = frozenset([("compression", comp), ("nthreads", nthreads)])
-    else:
-        key = frozenset([("compression", comp)])
-    if key in _desc_map:
-        return _desc_map[key]
-    if nthreads is not None:
-        desc = get_nthreads_descriptor(nthreads, _cache=False)
-    else:
-        desc_obj = ffi.new("GrB_Descriptor*")
-        lib.GrB_Descriptor_new(desc_obj)
-        desc = Descriptor(desc_obj[0], "")
-    name = f"compression_{compression}{level or ''}"
-    if nthreads and nthreads > 0:
-        name += f"_nthreads{nthreads}"
-    desc.name = name
     check_status(
         lib.GxB_Desc_set(
             desc._carg,
@@ -84,5 +199,4 @@ def get_compression_descriptor(compression="default", level=None, nthreads=None)
         ),
         desc,
     )
-    _desc_map[key] = desc
     return desc
diff --git a/graphblas/core/ss/matrix.py b/graphblas/core/ss/matrix.py
index 9be0ccbdc..9407fb65f 100644
--- a/graphblas/core/ss/matrix.py
+++ b/graphblas/core/ss/matrix.py
@@ -14,7 +14,6 @@
 from ...exceptions import _error_code_lookup, check_status, check_status_carg
 from .. import NULL, ffi, lib
 from ..base import call
-from ..descriptor import lookup as descriptor_lookup
 from ..operator import get_typed_op
 from ..scalar import Scalar, _as_scalar, _scalar_index
 from ..utils import (
@@ -24,14 +23,13 @@
     get_order,
     get_shape,
     ints_to_numpy_buffer,
-    libget,
     normalize_chunks,
     output_type,
     values_to_numpy_buffer,
     wrapdoc,
 )
 from .config import BaseConfig
-from .descriptor import get_compression_descriptor, get_nthreads_descriptor, set_nthreads
+from .descriptor import get_descriptor
 
 ffi_new = ffi.new
 
@@ -147,24 +145,24 @@ class MatrixConfig(BaseConfig):
     }
     _bitwise = {
         "sparsity_control": {
-            lib.GxB_HYPERSPARSE: "hypersparse",
-            lib.GxB_SPARSE: "sparse",
-            lib.GxB_BITMAP: "bitmap",
-            lib.GxB_FULL: "full",
-            lib.GxB_AUTO_SPARSITY: "auto",
+            "hypersparse": lib.GxB_HYPERSPARSE,
+            "sparse": lib.GxB_SPARSE,
+            "bitmap": lib.GxB_BITMAP,
+            "full": lib.GxB_FULL,
+            "auto": lib.GxB_AUTO_SPARSITY,
         },
     }
     _enumerations = {
         "format": {
-            lib.GxB_BY_ROW: "by_row",
-            lib.GxB_BY_COL: "by_col",
-            # lib.GxB_NO_FORMAT: "no_format",  # Used by iterators; not valid here
+            "by_row": lib.GxB_BY_ROW,
+            "by_col": lib.GxB_BY_COL,
+            # "no_format": lib.GxB_NO_FORMAT,  # Used by iterators; not valid here
         },
         "sparsity_status": {
-            lib.GxB_HYPERSPARSE: "hypersparse",
-            lib.GxB_SPARSE: "sparse",
-            lib.GxB_BITMAP: "bitmap",
-            lib.GxB_FULL: "full",
+            "hypersparse": lib.GxB_HYPERSPARSE,
+            "sparse": lib.GxB_SPARSE,
+            "bitmap": lib.GxB_BITMAP,
+            "full": lib.GxB_FULL,
         },
     }
     _defaults = {
@@ -244,7 +242,7 @@ def orientation(self):
             return "columnwise"
         return "rowwise"
 
-    def build_diag(self, vector, k=0):
+    def build_diag(self, vector, k=0, **opts):
         """
         GxB_Matrix_diag
 
@@ -268,9 +266,12 @@ def build_diag(self, vector, k=0):
         vector = self._parent._expect_type(
             vector, gb.Vector, within="ss.build_diag", argname="vector"
         )
-        call("GxB_Matrix_diag", [self._parent, vector, _as_scalar(k, INT64, is_cscalar=True), None])
+        call(
+            "GxB_Matrix_diag",
+            [self._parent, vector, _as_scalar(k, INT64, is_cscalar=True), get_descriptor(**opts)],
+        )
 
-    def split(self, chunks, *, name=None):
+    def split(self, chunks, *, name=None, **opts):
         """
         GxB_Matrix_split
 
@@ -310,7 +311,7 @@ def split(self, chunks, *, name=None):
                 _CArray(tile_nrows),
                 _CArray(tile_ncols),
                 self._parent,
-                None,
+                get_descriptor(**opts),
             ],
         )
         rv = []
@@ -330,7 +331,7 @@ def split(self, chunks, *, name=None):
             rv.append(cur)
         return rv
 
-    def _concat(self, tiles, m, n):
+    def _concat(self, tiles, m, n, opts):
         from ..matrix import TransposedMatrix
 
         ctiles = ffi.new("GrB_Matrix[]", m * n)
@@ -348,11 +349,11 @@ def _concat(self, tiles, m, n):
                 _MatrixArray(ctiles, name="tiles"),
                 _as_scalar(m, _INDEX, is_cscalar=True),
                 _as_scalar(n, _INDEX, is_cscalar=True),
-                None,
+                get_descriptor(**opts),
             ],
         )
 
-    def concat(self, tiles):
+    def concat(self, tiles, **opts):
         """
         GxB_Matrix_concat
 
@@ -370,7 +371,7 @@ def concat(self, tiles):
         graphblas.ss.concat
         """
         tiles, m, n, is_matrix = _concat_mn(tiles, is_matrix=True)
-        self._concat(tiles, m, n)
+        self._concat(tiles, m, n, opts)
 
     def build_scalar(self, rows, columns, value):
         """
@@ -530,7 +531,7 @@ def iteritems(self, seek=0):
         finally:
             lib.GxB_Iterator_free(it_ptr)
 
-    def export(self, format=None, *, sort=False, give_ownership=False, raw=False):
+    def export(self, format=None, *, sort=False, give_ownership=False, raw=False, **opts):
         """
         GxB_Matrix_export_xxx
 
@@ -715,10 +716,15 @@ def export(self, format=None, *, sort=False, give_ownership=False, raw=False):
         >>> A2 = Matrix.ss.import_any(**pieces)
         """
         return self._export(
-            format, sort=sort, give_ownership=give_ownership, raw=raw, method="export"
+            format,
+            sort=sort,
+            give_ownership=give_ownership,
+            raw=raw,
+            method="export",
+            opts=opts,
         )
 
-    def unpack(self, format=None, *, sort=False, raw=False):
+    def unpack(self, format=None, *, sort=False, raw=False, **opts):
         """
         GxB_Matrix_unpack_xxx
 
@@ -727,9 +733,11 @@ def unpack(self, format=None, *, sort=False, raw=False):
 
         See `Matrix.ss.export` documentation for more details.
         """
-        return self._export(format, sort=sort, raw=raw, give_ownership=True, method="unpack")
+        return self._export(
+            format, sort=sort, raw=raw, give_ownership=True, method="unpack", opts=opts
+        )
 
-    def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, method):
+    def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, method, opts):
         if format is None:
             format = self.format
         else:
@@ -749,6 +757,8 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
             parent = self._parent.dup(name=f"M_{method}")
         dtype = parent.dtype.np_type
         index_dtype = np.dtype(np.uint64)
+        desc = get_descriptor(**opts)
+        desc_obj = NULL if desc is None else desc._carg
 
         nrows = parent._nrows
         ncols = parent._ncols
@@ -809,7 +819,12 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                         parent.clear()
             elif format == "coor":
                 rv = self._export(
-                    "csr", sort=sort, give_ownership=give_ownership, raw=False, method=method
+                    "csr",
+                    sort=sort,
+                    give_ownership=give_ownership,
+                    raw=False,
+                    method=method,
+                    opts=opts,
                 )
                 rv["rows"] = indptr_to_indices(rv.pop("indptr"))
                 rv["cols"] = rv.pop("col_indices")
@@ -817,7 +832,12 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                 rv["format"] = "coor"
             elif format == "cooc":
                 rv = self._export(
-                    "csc", sort=sort, give_ownership=give_ownership, raw=False, method=method
+                    "csc",
+                    sort=sort,
+                    give_ownership=give_ownership,
+                    raw=False,
+                    method=method,
+                    opts=opts,
                 )
                 rv["cols"] = indptr_to_indices(rv.pop("indptr"))
                 rv["rows"] = rv.pop("row_indices")
@@ -852,7 +872,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
             Aj = ffi_new("GrB_Index**")
             Aj_size = ffi_new("GrB_Index*")
             check_status(
-                libget(f"GxB_Matrix_{method}_CSR")(
+                getattr(lib, f"GxB_Matrix_{method}_CSR")(
                     mhandle,
                     *args,
                     Ap,
@@ -863,7 +883,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                     Ax_size,
                     is_iso,
                     jumbled,
-                    NULL,
+                    desc_obj,
                 ),
                 parent,
             )
@@ -894,7 +914,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
             Ai = ffi_new("GrB_Index**")
             Ai_size = ffi_new("GrB_Index*")
             check_status(
-                libget(f"GxB_Matrix_{method}_CSC")(
+                getattr(lib, f"GxB_Matrix_{method}_CSC")(
                     mhandle,
                     *args,
                     Ap,
@@ -905,7 +925,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                     Ax_size,
                     is_iso,
                     jumbled,
-                    NULL,
+                    desc_obj,
                 ),
                 parent,
             )
@@ -939,7 +959,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
             Ah_size = ffi_new("GrB_Index*")
             Aj_size = ffi_new("GrB_Index*")
             check_status(
-                libget(f"GxB_Matrix_{method}_HyperCSR")(
+                getattr(lib, f"GxB_Matrix_{method}_HyperCSR")(
                     mhandle,
                     *args,
                     Ap,
@@ -953,7 +973,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                     is_iso,
                     nvec,
                     jumbled,
-                    NULL,
+                    desc_obj,
                 ),
                 parent,
             )
@@ -994,7 +1014,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
             Ah_size = ffi_new("GrB_Index*")
             Ai_size = ffi_new("GrB_Index*")
             check_status(
-                libget(f"GxB_Matrix_{method}_HyperCSC")(
+                getattr(lib, f"GxB_Matrix_{method}_HyperCSC")(
                     mhandle,
                     *args,
                     Ap,
@@ -1008,7 +1028,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                     is_iso,
                     nvec,
                     jumbled,
-                    NULL,
+                    desc_obj,
                 ),
                 parent,
             )
@@ -1044,9 +1064,9 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                 rv["nvec"] = nvec
         elif format == "bitmapr" or format == "bitmapc":
             if format == "bitmapr":
-                cfunc = libget(f"GxB_Matrix_{method}_BitmapR")
+                cfunc = getattr(lib, f"GxB_Matrix_{method}_BitmapR")
             else:
-                cfunc = libget(f"GxB_Matrix_{method}_BitmapC")
+                cfunc = getattr(lib, f"GxB_Matrix_{method}_BitmapC")
             Ab = ffi_new("int8_t**")
             Ab_size = ffi_new("GrB_Index*")
             nvals_ = ffi_new("GrB_Index*")
@@ -1060,7 +1080,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                     Ax_size,
                     is_iso,
                     nvals_,
-                    NULL,
+                    desc_obj,
                 ),
                 parent,
             )
@@ -1100,9 +1120,9 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                 rv["ncols"] = ncols
         elif format == "fullr" or format == "fullc":
             if format == "fullr":
-                cfunc = libget(f"GxB_Matrix_{method}_FullR")
+                cfunc = getattr(lib, f"GxB_Matrix_{method}_FullR")
             else:
-                cfunc = libget(f"GxB_Matrix_{method}_FullC")
+                cfunc = getattr(lib, f"GxB_Matrix_{method}_FullC")
             check_status(
                 cfunc(
                     mhandle,
@@ -1110,7 +1130,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                     Ax,
                     Ax_size,
                     is_iso,
-                    NULL,
+                    desc_obj,
                 ),
                 parent,
             )
@@ -1153,9 +1173,11 @@ def import_csr(
         is_iso=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_CSR
@@ -1208,10 +1230,12 @@ def import_csr(
             is_iso=is_iso,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_csr(
@@ -1223,8 +1247,14 @@ def pack_csr(
         is_iso=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **ignored_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_CSR
@@ -1241,9 +1271,11 @@ def pack_csr(
             is_iso=is_iso,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -1258,11 +1290,13 @@ def _import_csr(
         is_iso=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "csr":
             raise ValueError(f"Invalid format: {format!r}.  Must be None or 'csr'.")
@@ -1287,7 +1321,8 @@ def _import_csr(
         else:
             mhandle = matrix._carg
             args = ()
-        status = libget(f"GxB_Matrix_{method}_CSR")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Matrix_{method}_CSR")(
             mhandle,
             *args,
             Ap,
@@ -1298,7 +1333,7 @@ def _import_csr(
             values.nbytes,
             is_iso,
             not sorted_cols,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -1326,9 +1361,11 @@ def import_csc(
         is_iso=False,
         sorted_rows=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_CSC
@@ -1381,10 +1418,12 @@ def import_csc(
             is_iso=is_iso,
             sorted_rows=sorted_rows,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_csc(
@@ -1396,8 +1435,14 @@ def pack_csc(
         is_iso=False,
         sorted_rows=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **ignored_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_CSC
@@ -1414,9 +1459,11 @@ def pack_csc(
             is_iso=is_iso,
             sorted_rows=sorted_rows,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -1431,11 +1478,13 @@ def _import_csc(
         is_iso=False,
         sorted_rows=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "csc":
             raise ValueError(f"Invalid format: {format!r}  Must be None or 'csc'.")
@@ -1460,7 +1509,8 @@ def _import_csc(
         else:
             mhandle = matrix._carg
             args = ()
-        status = libget(f"GxB_Matrix_{method}_CSC")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Matrix_{method}_CSC")(
             mhandle,
             *args,
             Ap,
@@ -1471,7 +1521,7 @@ def _import_csc(
             values.nbytes,
             is_iso,
             not sorted_rows,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -1501,9 +1551,11 @@ def import_hypercsr(
         is_iso=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_HyperCSR
@@ -1562,10 +1614,12 @@ def import_hypercsr(
             is_iso=is_iso,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_hypercsr(
@@ -1579,8 +1633,14 @@ def pack_hypercsr(
         is_iso=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **ignored_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_HyperCSR
@@ -1599,9 +1659,11 @@ def pack_hypercsr(
             is_iso=is_iso,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -1618,11 +1680,13 @@ def _import_hypercsr(
         is_iso=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "hypercsr":
             raise ValueError(f"Invalid format: {format!r}  Must be None or 'hypercsr'.")
@@ -1663,7 +1727,8 @@ def _import_hypercsr(
         else:
             mhandle = matrix._carg
             args = ()
-        status = libget(f"GxB_Matrix_{method}_HyperCSR")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Matrix_{method}_HyperCSR")(
             mhandle,
             *args,
             Ap,
@@ -1677,7 +1742,7 @@ def _import_hypercsr(
             is_iso,
             nvec,
             not sorted_cols,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -1708,9 +1773,11 @@ def import_hypercsc(
         is_iso=False,
         sorted_rows=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_HyperCSC
@@ -1768,10 +1835,12 @@ def import_hypercsc(
             is_iso=is_iso,
             sorted_rows=sorted_rows,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_hypercsc(
@@ -1785,8 +1854,14 @@ def pack_hypercsc(
         is_iso=False,
         sorted_rows=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **ignored_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_HyperCSC
@@ -1805,9 +1880,11 @@ def pack_hypercsc(
             is_iso=is_iso,
             sorted_rows=sorted_rows,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -1824,11 +1901,13 @@ def _import_hypercsc(
         is_iso=False,
         sorted_rows=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "hypercsc":
             raise ValueError(f"Invalid format: {format!r}  Must be None or 'hypercsc'.")
@@ -1869,7 +1948,8 @@ def _import_hypercsc(
         else:
             mhandle = matrix._carg
             args = ()
-        status = libget(f"GxB_Matrix_{method}_HyperCSC")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Matrix_{method}_HyperCSC")(
             mhandle,
             *args,
             Ap,
@@ -1883,7 +1963,7 @@ def _import_hypercsc(
             is_iso,
             nvec,
             not sorted_rows,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -1911,9 +1991,11 @@ def import_bitmapr(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_BitmapR
@@ -1971,10 +2053,12 @@ def import_bitmapr(
             ncols=ncols,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_bitmapr(
@@ -1985,8 +2069,14 @@ def pack_bitmapr(
         nvals=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_BitmapR
@@ -2002,9 +2092,11 @@ def pack_bitmapr(
             nvals=nvals,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -2018,11 +2110,13 @@ def _import_bitmapr(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "bitmapr":
             raise ValueError(f"Invalid format: {format!r}  Must be None or 'bitmapr'.")
@@ -2052,7 +2146,8 @@ def _import_bitmapr(
         else:
             mhandle = matrix._carg
             args = ()
-        status = libget(f"GxB_Matrix_{method}_BitmapR")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Matrix_{method}_BitmapR")(
             mhandle,
             *args,
             Ab,
@@ -2061,7 +2156,7 @@ def _import_bitmapr(
             values.nbytes,
             is_iso,
             nvals,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -2087,9 +2182,11 @@ def import_bitmapc(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_BitmapC
@@ -2147,10 +2244,12 @@ def import_bitmapc(
             ncols=ncols,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_bitmapc(
@@ -2161,8 +2260,14 @@ def pack_bitmapc(
         nvals=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_BitmapC
@@ -2178,9 +2283,11 @@ def pack_bitmapc(
             nvals=nvals,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -2194,11 +2301,13 @@ def _import_bitmapc(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "bitmapc":
             raise ValueError(f"Invalid format: {format!r}  Must be None or 'bitmapc'.")
@@ -2228,7 +2337,8 @@ def _import_bitmapc(
         else:
             mhandle = matrix._carg
             args = ()
-        status = libget(f"GxB_Matrix_{method}_BitmapC")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Matrix_{method}_BitmapC")(
             mhandle,
             *args,
             Ab,
@@ -2237,7 +2347,7 @@ def _import_bitmapc(
             values.nbytes,
             is_iso,
             nvals,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -2261,9 +2371,11 @@ def import_fullr(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_FullR
@@ -2314,10 +2426,12 @@ def import_fullr(
             ncols=ncols,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_fullr(
@@ -2326,8 +2440,14 @@ def pack_fullr(
         *,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_FullR
@@ -2341,9 +2461,11 @@ def pack_fullr(
             values=values,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -2355,11 +2477,13 @@ def _import_fullr(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "fullr":
             raise ValueError(f"Invalid format: {format!r}  Must be None or 'fullr'.")
@@ -2379,13 +2503,9 @@ def _import_fullr(
         else:
             mhandle = matrix._carg
             args = ()
-        status = libget(f"GxB_Matrix_{method}_FullR")(
-            mhandle,
-            *args,
-            Ax,
-            values.nbytes,
-            is_iso,
-            NULL,
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Matrix_{method}_FullR")(
+            mhandle, *args, Ax, values.nbytes, is_iso, NULL if desc is None else desc._carg
         )
         if method == "import":
             check_status_carg(
@@ -2408,9 +2528,11 @@ def import_fullc(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_FullC
@@ -2461,10 +2583,12 @@ def import_fullc(
             ncols=ncols,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_fullc(
@@ -2473,8 +2597,14 @@ def pack_fullc(
         *,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_FullC
@@ -2488,9 +2618,11 @@ def pack_fullc(
             values=values,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -2502,11 +2634,13 @@ def _import_fullc(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "fullc":
             raise ValueError(f"Invalid format: {format!r}.  Must be None or 'fullc'.")
@@ -2525,13 +2659,9 @@ def _import_fullc(
         else:
             mhandle = matrix._carg
             args = ()
-        status = libget(f"GxB_Matrix_{method}_FullC")(
-            mhandle,
-            *args,
-            Ax,
-            values.nbytes,
-            is_iso,
-            NULL,
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Matrix_{method}_FullC")(
+            mhandle, *args, Ax, values.nbytes, is_iso, NULL if desc is None else desc._carg
         )
         if method == "import":
             check_status_carg(
@@ -2558,9 +2688,11 @@ def import_coo(
         sorted_rows=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GrB_Matrix_build_XXX and GxB_Matrix_build_Scalar
@@ -2608,10 +2740,12 @@ def import_coo(
             sorted_rows=sorted_rows,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_coo(
@@ -2624,8 +2758,14 @@ def pack_coo(
         sorted_rows=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GrB_Matrix_build_XXX and GxB_Matrix_build_Scalar
@@ -2645,9 +2785,11 @@ def pack_coo(
             sorted_rows=sorted_rows,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -2663,11 +2805,13 @@ def _import_coo(
         sorted_rows=False,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "coo":
             raise ValueError(f"Invalid format: {format!r}.  Must be None or 'coo'.")
@@ -2681,10 +2825,12 @@ def _import_coo(
                 is_iso=is_iso,
                 sorted_cols=sorted_cols,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
                 method=method,
                 matrix=matrix,
+                opts=opts,
             )
         if sorted_cols and (not sorted_rows or issorted(cols)):
             return cls._import_cooc(
@@ -2696,10 +2842,12 @@ def _import_coo(
                 is_iso=is_iso,
                 sorted_rows=sorted_rows,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
                 method=method,
                 matrix=matrix,
+                opts=opts,
             )
 
         if method == "pack":
@@ -2726,9 +2874,11 @@ def import_coor(
         sorted_rows=True,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_CSR
@@ -2786,10 +2936,12 @@ def import_coor(
             sorted_rows=sorted_rows,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_coor(
@@ -2802,8 +2954,14 @@ def pack_coor(
         sorted_rows=True,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_CSR
@@ -2822,9 +2980,11 @@ def pack_coor(
             sorted_rows=sorted_rows,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -2840,11 +3000,13 @@ def _import_coor(
         sorted_rows=True,
         sorted_cols=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "coor":
             raise ValueError(f"Invalid format: {format!r}.  Must be None or 'coor'.")
@@ -2860,10 +3022,12 @@ def _import_coor(
             is_iso=is_iso,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             name=name,
             method=method,
             matrix=matrix,
+            opts=opts,
         )
 
     @classmethod
@@ -2879,9 +3043,11 @@ def import_cooc(
         sorted_rows=False,
         sorted_cols=True,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_import_CSC
@@ -2939,10 +3105,12 @@ def import_cooc(
             sorted_rows=sorted_rows,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_cooc(
@@ -2955,8 +3123,14 @@ def pack_cooc(
         sorted_rows=False,
         sorted_cols=True,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        nrows=None,
+        ncols=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_CSC
@@ -2975,9 +3149,11 @@ def pack_cooc(
             sorted_rows=sorted_rows,
             sorted_cols=sorted_cols,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -2993,11 +3169,13 @@ def _import_cooc(
         sorted_rows=False,
         sorted_cols=True,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         matrix=None,
+        opts,
     ):
         if format is not None and format.lower() != "cooc":
             raise ValueError(f"Invalid format: {format!r}.  Must be None or 'cooc'.")
@@ -3013,10 +3191,12 @@ def _import_cooc(
             is_iso=is_iso,
             sorted_rows=sorted_rows,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             name=name,
             method=method,
             matrix=matrix,
+            opts=opts,
         )
 
     @classmethod
@@ -3029,6 +3209,7 @@ def import_any(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
         dtype=None,
         name=None,
@@ -3051,6 +3232,7 @@ def import_any(
         # BitmapR/BitmapC
         bitmap=None,
         nvals=None,  # optional
+        **opts,
     ):
         """
         GxB_Matrix_import_xxx
@@ -3088,6 +3270,7 @@ def import_any(
             ncols=ncols,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             dtype=dtype,
             name=name,
@@ -3111,6 +3294,7 @@ def import_any(
             bitmap=bitmap,
             nvals=nvals,
             method="import",
+            opts=opts,
         )
 
     def pack_any(
@@ -3120,6 +3304,7 @@ def pack_any(
         values,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
         # CSR/CSC/HyperCSR/HyperCSC
         indptr=None,
@@ -3140,11 +3325,12 @@ def pack_any(
         # BitmapR/BitmapC
         bitmap=None,
         nvals=None,  # optional
-        # Unused for pack
+        # Unused for pack, ignored
         nrows=None,
         ncols=None,
         dtype=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Matrix_pack_xxx
@@ -3158,6 +3344,7 @@ def pack_any(
             values=values,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             # CSR/CSC/HyperCSR/HyperCSC
             indptr=indptr,
@@ -3180,6 +3367,7 @@ def pack_any(
             nvals=nvals,
             method="pack",
             matrix=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -3192,6 +3380,7 @@ def _import_any(
         ncols=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
         dtype=None,
         name=None,
@@ -3216,6 +3405,7 @@ def _import_any(
         nvals=None,  # optional
         method,
         matrix=None,
+        opts,
     ):
         if format is None:
             # Determine format based on provided inputs
@@ -3303,8 +3493,10 @@ def _import_any(
                 is_iso=is_iso,
                 sorted_cols=sorted_cols,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "csc":
             return getattr(obj, f"{method}_csc")(
@@ -3316,8 +3508,10 @@ def _import_any(
                 is_iso=is_iso,
                 sorted_rows=sorted_rows,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "hypercsr":
             return getattr(obj, f"{method}_hypercsr")(
@@ -3331,8 +3525,10 @@ def _import_any(
                 is_iso=is_iso,
                 sorted_cols=sorted_cols,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "hypercsc":
             return getattr(obj, f"{method}_hypercsc")(
@@ -3346,8 +3542,10 @@ def _import_any(
                 is_iso=is_iso,
                 sorted_rows=sorted_rows,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "bitmapr":
             return getattr(obj, f"{method}_bitmapr")(
@@ -3358,8 +3556,10 @@ def _import_any(
                 bitmap=bitmap,
                 is_iso=is_iso,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "bitmapc":
             return getattr(obj, f"{method}_bitmapc")(
@@ -3370,8 +3570,10 @@ def _import_any(
                 bitmap=bitmap,
                 is_iso=is_iso,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "fullr":
             return getattr(obj, f"{method}_fullr")(
@@ -3380,8 +3582,10 @@ def _import_any(
                 values=values,
                 is_iso=is_iso,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "fullc":
             return getattr(obj, f"{method}_fullc")(
@@ -3390,8 +3594,10 @@ def _import_any(
                 values=values,
                 is_iso=is_iso,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "coo":
             return getattr(obj, f"{method}_coo")(
@@ -3404,8 +3610,10 @@ def _import_any(
                 sorted_rows=sorted_rows,
                 sorted_cols=sorted_cols,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "coor":
             return getattr(obj, f"{method}_coor")(
@@ -3418,8 +3626,10 @@ def _import_any(
                 sorted_rows=sorted_rows,
                 sorted_cols=sorted_cols,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "cooc":
             return getattr(obj, f"{method}_cooc")(
@@ -3432,12 +3642,14 @@ def _import_any(
                 sorted_rows=sorted_rows,
                 sorted_cols=sorted_cols,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         raise ValueError(f"Invalid format: {format}")
 
-    def unpack_hyperhash(self, *, compute=False, name=None):
+    def unpack_hyperhash(self, *, compute=False, name=None, **opts):
         """Unpacks the hyper_hash of a hypersparse matrix if possible.
 
         Will return None if the matrix is not hypersparse or if the hash is not computed.
@@ -3454,27 +3666,27 @@ def unpack_hyperhash(self, *, compute=False, name=None):
         if compute and self.format.startswith("hypercs"):
             self._parent.wait()
         rv = Matrix._from_obj(ffi_new("GrB_Matrix*"), INT64, 0, 0, name=name)
-        call("GxB_unpack_HyperHash", [self._parent, _Pointer(rv), None])
+        call("GxB_unpack_HyperHash", [self._parent, _Pointer(rv), get_descriptor(**opts)])
         if rv.gb_obj[0] == NULL:
             return
         rv._nrows = rv.nrows
         rv._ncols = rv.ncols
         return rv
 
-    def pack_hyperhash(self, Y):
+    def pack_hyperhash(self, Y, **opts):
         """Pack a hyper_hash matrix Y into the current hypersparse matrix.
 
         The hyper_hash matrix Y should be from ``unpack_hyperhash`` and unmodified.
 
         This uses move semantics. Y will become an invalid matrix.
         """
-        call("GxB_pack_HyperHash", [self._parent, _Pointer(Y), None])
+        call("GxB_pack_HyperHash", [self._parent, _Pointer(Y), get_descriptor(**opts)])
 
     @wrapdoc(head)
     def head(self, n=10, dtype=None, *, sort=False):
         return head(self._parent, n, dtype, sort=sort)
 
-    def scan(self, op=monoid.plus, order="rowwise", *, name=None):
+    def scan(self, op=monoid.plus, order="rowwise", *, name=None, **opts):
         """Perform a prefix scan across rows (default) or columns with the given monoid.
 
         For example, use `monoid.plus` (the default) to perform a cumulative sum,
@@ -3488,9 +3700,9 @@ def scan(self, op=monoid.plus, order="rowwise", *, name=None):
         parent = self._parent
         if order == "columnwise":
             parent = parent.T
-        return prefix_scan(parent, op, name=name, within="scan")
+        return prefix_scan(parent, op, name=name, within="scan", **opts)
 
-    def scan_columnwise(self, op=monoid.plus, *, name=None):
+    def scan_columnwise(self, op=monoid.plus, *, name=None, **opts):
         """Perform a prefix scan across columns with the given monoid.
 
         .. deprecated:: 2022.11.1
@@ -3510,9 +3722,9 @@ def scan_columnwise(self, op=monoid.plus, *, name=None):
             'please use `Matrix.ss.scan(order="columnwise")` instead.',
             DeprecationWarning,
         )
-        return prefix_scan(self._parent.T, op, name=name, within="scan_columnwise")
+        return prefix_scan(self._parent.T, op, name=name, within="scan_columnwise", **opts)
 
-    def scan_rowwise(self, op=monoid.plus, *, name=None):
+    def scan_rowwise(self, op=monoid.plus, *, name=None, **opts):
         """Perform a prefix scan across rows with the given monoid.
 
         .. deprecated:: 2022.11.1
@@ -3531,9 +3743,9 @@ def scan_rowwise(self, op=monoid.plus, *, name=None):
             "`Matrix.ss.scan_rowwise` is deprecated; please use `Matrix.ss.scan` instead.",
             DeprecationWarning,
         )
-        return prefix_scan(self._parent, op, name=name, within="scan_rowwise")
+        return prefix_scan(self._parent, op, name=name, within="scan_rowwise", **opts)
 
-    def flatten(self, order="rowwise", *, name=None):
+    def flatten(self, order="rowwise", *, name=None, **opts):
         """Return a copy of the Matrix collapsed into a Vector.
 
         Parameters
@@ -3555,10 +3767,10 @@ def flatten(self, order="rowwise", *, name=None):
         --------
         Vector.ss.reshape : copy a Vector to a Matrix.
         """
-        rv = self.reshape(-1, 1, order=order, name=name)
+        rv = self.reshape(-1, 1, order=order, name=name, **opts)
         return rv._as_vector()
 
-    def reshape(self, nrows, ncols=None, order="rowwise", *, inplace=False, name=None):
+    def reshape(self, nrows, ncols=None, order="rowwise", *, inplace=False, name=None, **opts):
         """Return a copy of Matrix with a new shape without changing its data.
 
         The shape of the Matrix must be compatible with the original shape.
@@ -3611,7 +3823,7 @@ def reshape(self, nrows, ncols=None, order="rowwise", *, inplace=False, name=Non
                     _as_scalar(order == "columnwise", BOOL, is_cscalar=True),
                     _as_scalar(nrows, _INDEX, is_cscalar=True),
                     _as_scalar(ncols, _INDEX, is_cscalar=True),
-                    None,
+                    get_descriptor(**opts),
                 ],
             )
             parent._nrows = nrows
@@ -3626,7 +3838,7 @@ def reshape(self, nrows, ncols=None, order="rowwise", *, inplace=False, name=Non
                 _as_scalar(order == "columnwise", BOOL, is_cscalar=True),
                 _as_scalar(nrows, _INDEX, is_cscalar=True),
                 _as_scalar(ncols, _INDEX, is_cscalar=True),
-                None,
+                get_descriptor(**opts),
             ],
         )
         return rv
@@ -3981,7 +4193,7 @@ def _compactify(self, how, reverse, asindex, nkey, nval, fmt, indices_name, name
             name=name,
         )
 
-    def sort(self, op=binary.lt, order="rowwise", *, values=True, permutation=True, nthreads=None):
+    def sort(self, op=binary.lt, order="rowwise", *, values=True, permutation=True, **opts):
         """GxB_Matrix_sort to sort values along the rows (default) or columns of the Matrix
 
         Sorting moves all the elements to the left (if rowwise) or top (if columnwise) just
@@ -4034,30 +4246,20 @@ def sort(self, op=binary.lt, order="rowwise", *, values=True, permutation=True,
             P = Matrix(UINT64, parent._nrows, parent._ncols, name="Permutation")
         else:
             P = None
-        # TODO: clean this up once we expose backend descriptors
-        if nthreads is not None:
-            if order == "rowwise":
-                desc = get_nthreads_descriptor(nthreads)
-            else:
-                desc = descriptor_lookup(transpose_first=True, create=True)
-                set_nthreads(desc, nthreads)
-        elif order == "rowwise":
-            desc = None
-        else:
-            desc = descriptor_lookup(transpose_first=True)
+        desc = get_descriptor(transpose_first=order == "columnwise", **opts)
         check_status(
             lib.GxB_Matrix_sort(
-                C._carg if C is not None else NULL,
-                P._carg if P is not None else NULL,
+                NULL if C is None else C._carg,
+                NULL if P is None else P._carg,
                 op._carg,
                 parent._carg,
-                desc._carg if desc is not None else NULL,
+                NULL if desc is None else desc._carg,
             ),
             parent,
         )
         return C, P
 
-    def serialize(self, compression="default", level=None, *, nthreads=None):
+    def serialize(self, compression="default", level=None, **opts):
         """Serialize a Matrix to bytes (as numpy array) using SuiteSparse GxB_Matrix_serialize.
 
         Parameters
@@ -4086,7 +4288,7 @@ def serialize(self, compression="default", level=None, *, nthreads=None):
 
         *Warning*: Behavior of serializing UDTs is experimental and may change in a future release.
         """
-        desc = get_compression_descriptor(compression, level=level, nthreads=nthreads)
+        desc = get_descriptor(compression=compression, compression_level=level, **opts)
         blob_handle = ffi_new("void**")
         blob_size_handle = ffi_new("GrB_Index*")
         parent = self._parent
@@ -4095,14 +4297,14 @@ def serialize(self, compression="default", level=None, *, nthreads=None):
                 blob_handle,
                 blob_size_handle,
                 parent._carg,
-                desc._carg,
+                NULL if desc is None else desc._carg,
             ),
             parent,
         )
         return claim_buffer(ffi, blob_handle[0], blob_size_handle[0], np.dtype(np.uint8))
 
     @classmethod
-    def deserialize(cls, data, dtype=None, *, nthreads=None, name=None):
+    def deserialize(cls, data, dtype=None, *, name=None, **opts):
         """Deserialize a Matrix from bytes, buffer, or numpy array using GxB_Matrix_deserialize.
 
         The data should have been previously serialized with a compatible version of
@@ -4144,13 +4346,12 @@ def deserialize(cls, data, dtype=None, *, nthreads=None, name=None):
             dtype = _string_to_dtype(dtype_name)
         else:
             dtype = lookup_dtype(dtype)
-        if nthreads is not None:
-            desc_obj = get_nthreads_descriptor(nthreads)._carg
-        else:
-            desc_obj = NULL
+        desc = get_descriptor(**opts)
         gb_obj = ffi_new("GrB_Matrix*")
         check_status_carg(
-            lib.GxB_Matrix_deserialize(gb_obj, dtype._carg, data_obj, data.nbytes, desc_obj),
+            lib.GxB_Matrix_deserialize(
+                gb_obj, dtype._carg, data_obj, data.nbytes, NULL if desc is None else desc._carg
+            ),
             "Matrix",
             gb_obj[0],
         )
diff --git a/graphblas/core/ss/prefix_scan.py b/graphblas/core/ss/prefix_scan.py
index b257959d3..fd320b062 100644
--- a/graphblas/core/ss/prefix_scan.py
+++ b/graphblas/core/ss/prefix_scan.py
@@ -9,7 +9,7 @@
 
 # By default, scans on matrices are done along rows.
 # To perform scans along columns, pass a transposed matrix.
-def prefix_scan(A, monoid, *, name=None, within):
+def prefix_scan(A, monoid, *, name=None, within, **opts):
     from ..matrix import Matrix, TransposedMatrix
     from ..vector import Vector
 
@@ -81,7 +81,7 @@ def prefix_scan(A, monoid, *, name=None, within):
         take_ownership=True,
         name="Up_0",
     )
-    B = semiring(A @ S).new(name="B")
+    B = semiring(A @ S).new(name="B", **opts)
     if is_vector:
         mask = None
     else:
@@ -106,7 +106,7 @@ def prefix_scan(A, monoid, *, name=None, within):
             take_ownership=True,
             name=f"Up_{index}",
         )
-        B(binaryop, mask=mask) << semiring(B @ S)
+        B(binaryop, mask=mask, **opts) << semiring(B @ S)
         index += 1
         stride = stride2
         stride2 *= 2
@@ -136,7 +136,7 @@ def prefix_scan(A, monoid, *, name=None, within):
                 take_ownership=True,
                 name=f"Down_{index}",
             )
-            B(binaryop, mask=mask) << semiring(B @ S)
+            B(binaryop, mask=mask, **opts) << semiring(B @ S)
             index += 1
             stride2 = stride
             stride //= 2
@@ -156,7 +156,7 @@ def prefix_scan(A, monoid, *, name=None, within):
         take_ownership=True,
         name=f"Down_{index}",
     )
-    RV = semiring(B @ S).new(mask=A.S, name="RV")
+    RV = semiring(B @ S).new(mask=A.S, name="RV", **opts)
 
     indices = np.arange(0, N_cols, 2, dtype=index_t)
     d = Vector.ss.import_sparse(
@@ -169,7 +169,7 @@ def prefix_scan(A, monoid, *, name=None, within):
         name="d",
     )
     D = d.diag(name="D")
-    RV(binaryop) << semiring(A @ D)
+    RV(binaryop, **opts) << semiring(A @ D)
     # De-compactify into final result
     if is_vector:
         rv_info = RV.ss.export("sparse", sort=True, give_ownership=True)
diff --git a/graphblas/core/ss/vector.py b/graphblas/core/ss/vector.py
index 857d421a1..581724189 100644
--- a/graphblas/core/ss/vector.py
+++ b/graphblas/core/ss/vector.py
@@ -18,13 +18,12 @@
     _CArray,
     _MatrixArray,
     ints_to_numpy_buffer,
-    libget,
     normalize_chunks,
     values_to_numpy_buffer,
     wrapdoc,
 )
 from .config import BaseConfig
-from .descriptor import get_compression_descriptor, get_nthreads_descriptor
+from .descriptor import get_descriptor
 from .matrix import _concat_mn
 from .prefix_scan import prefix_scan
 
@@ -75,24 +74,24 @@ class VectorConfig(BaseConfig):
     }
     _bitwise = {
         "sparsity_control": {
-            # lib.GxB_HYPERSPARSE: "hypersparse",  # For matrices, not vectors
-            lib.GxB_SPARSE: "sparse",
-            lib.GxB_BITMAP: "bitmap",
-            lib.GxB_FULL: "full",
-            lib.GxB_AUTO_SPARSITY: "auto",
+            # "hypersparse": lib.GxB_HYPERSPARSE,  # For matrices, not vectors
+            "sparse": lib.GxB_SPARSE,
+            "bitmap": lib.GxB_BITMAP,
+            "full": lib.GxB_FULL,
+            "auto": lib.GxB_AUTO_SPARSITY,
         },
     }
     _enumerations = {
         "format": {
-            lib.GxB_BY_ROW: "by_row",
-            lib.GxB_BY_COL: "by_col",
-            # lib.GxB_NO_FORMAT: "no_format",  # Used by iterators; not valid here
+            "by_row": lib.GxB_BY_ROW,
+            "by_col": lib.GxB_BY_COL,
+            # "no_format": lib.GxB_NO_FORMAT,  # Used by iterators; not valid here
         },
         "sparsity_status": {
-            lib.GxB_HYPERSPARSE: "hypersparse",
-            lib.GxB_SPARSE: "sparse",
-            lib.GxB_BITMAP: "bitmap",
-            lib.GxB_FULL: "full",
+            "hypersparse": lib.GxB_HYPERSPARSE,
+            "sparse": lib.GxB_SPARSE,
+            "bitmap": lib.GxB_BITMAP,
+            "full": lib.GxB_FULL,
         },
     }
     _defaults = {
@@ -146,7 +145,7 @@ def format(self):
             raise NotImplementedError(f"Unknown sparsity status: {sparsity_status}")
         return format
 
-    def build_diag(self, matrix, k=0):
+    def build_diag(self, matrix, k=0, **opts):
         """
         GxB_Vector_diag
 
@@ -179,9 +178,12 @@ def build_diag(self, matrix, k=0):
             # Transpose descriptor doesn't do anything, so use the parent
             k = -k
             matrix = matrix._matrix
-        call("GxB_Vector_diag", [self._parent, matrix, _as_scalar(k, INT64, is_cscalar=True), None])
+        call(
+            "GxB_Vector_diag",
+            [self._parent, matrix, _as_scalar(k, INT64, is_cscalar=True), get_descriptor(**opts)],
+        )
 
-    def split(self, chunks, *, name=None):
+    def split(self, chunks, *, name=None, **opts):
         """
         GxB_Matrix_split
 
@@ -217,7 +219,7 @@ def split(self, chunks, *, name=None):
                 _CArray(tile_nrows),
                 _CArray([1]),
                 parent,
-                None,
+                get_descriptor(**opts),
             ],
         )
         rv = []
@@ -232,7 +234,7 @@ def split(self, chunks, *, name=None):
             rv.append(tile)
         return rv
 
-    def _concat(self, tiles, m):
+    def _concat(self, tiles, m, opts):
         ctiles = ffi_new("GrB_Matrix[]", m)
         for i, tile in enumerate(tiles):
             ctiles[i] = tile.gb_obj[0]
@@ -243,11 +245,11 @@ def _concat(self, tiles, m):
                 _MatrixArray(ctiles, name="tiles"),
                 _as_scalar(m, _INDEX, is_cscalar=True),
                 _as_scalar(1, _INDEX, is_cscalar=True),
-                None,
+                get_descriptor(**opts),
             ],
         )
 
-    def concat(self, tiles):
+    def concat(self, tiles, **opts):
         """
         GxB_Matrix_concat
 
@@ -263,7 +265,7 @@ def concat(self, tiles):
         graphblas.ss.concat
         """
         tiles, m, n, is_matrix = _concat_mn(tiles, is_matrix=False)
-        self._concat(tiles, m)
+        self._concat(tiles, m, opts)
 
     def build_scalar(self, indices, value):
         """
@@ -411,7 +413,7 @@ def iteritems(self, seek=0):
         finally:
             lib.GxB_Iterator_free(it_ptr)
 
-    def export(self, format=None, *, sort=False, give_ownership=False, raw=False):
+    def export(self, format=None, *, sort=False, give_ownership=False, raw=False, **opts):
         """
         GxB_Vextor_export_xxx
 
@@ -472,10 +474,15 @@ def export(self, format=None, *, sort=False, give_ownership=False, raw=False):
         >>> v2 = Vector.ss.import_any(**pieces)
         """
         return self._export(
-            format=format, sort=sort, give_ownership=give_ownership, raw=raw, method="export"
+            format=format,
+            sort=sort,
+            give_ownership=give_ownership,
+            raw=raw,
+            method="export",
+            opts=opts,
         )
 
-    def unpack(self, format=None, *, sort=False, raw=False):
+    def unpack(self, format=None, *, sort=False, raw=False, **opts):
         """
         GxB_Vector_unpack_xxx
 
@@ -484,9 +491,11 @@ def unpack(self, format=None, *, sort=False, raw=False):
 
         See `Vector.ss.export` documentation for more details.
         """
-        return self._export(format=format, sort=sort, give_ownership=True, raw=raw, method="unpack")
+        return self._export(
+            format=format, sort=sort, give_ownership=True, raw=raw, method="unpack", opts=opts
+        )
 
-    def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, method):
+    def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, method, opts):
         if give_ownership:
             parent = self._parent
         else:
@@ -515,12 +524,14 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
         else:
             jumbled = ffi_new("bool*")
         is_iso = ffi_new("bool*")
+        desc = get_descriptor(**opts)
+        desc_obj = NULL if desc is None else desc._carg
         if format == "sparse":
             vi = ffi_new("GrB_Index**")
             vi_size = ffi_new("GrB_Index*")
             nvals = ffi_new("GrB_Index*")
             check_status(
-                libget(f"GxB_Vector_{method}_CSC")(
+                getattr(lib, f"GxB_Vector_{method}_CSC")(
                     vhandle,
                     *args,
                     vi,
@@ -530,7 +541,7 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                     is_iso,
                     nvals,
                     jumbled,
-                    NULL,
+                    desc_obj,
                 ),
                 parent,
             )
@@ -559,8 +570,16 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
             vb_size = ffi_new("GrB_Index*")
             nvals = ffi_new("GrB_Index*")
             check_status(
-                libget(f"GxB_Vector_{method}_Bitmap")(
-                    vhandle, *args, vb, vx, vb_size, vx_size, is_iso, nvals, NULL
+                getattr(lib, f"GxB_Vector_{method}_Bitmap")(
+                    vhandle,
+                    *args,
+                    vb,
+                    vx,
+                    vb_size,
+                    vx_size,
+                    is_iso,
+                    nvals,
+                    desc_obj,
                 ),
                 parent,
             )
@@ -585,7 +604,14 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m
                 rv["size"] = size
         elif format == "full":
             check_status(
-                libget(f"GxB_Vector_{method}_Full")(vhandle, *args, vx, vx_size, is_iso, NULL),
+                getattr(lib, f"GxB_Vector_{method}_Full")(
+                    vhandle,
+                    *args,
+                    vx,
+                    vx_size,
+                    is_iso,
+                    desc_obj,
+                ),
                 parent,
             )
             is_iso = is_iso[0]
@@ -623,6 +649,7 @@ def import_any(
         size=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
         dtype=None,
         name=None,
@@ -633,6 +660,7 @@ def import_any(
         bitmap=None,
         # Bitmap/Sparse
         nvals=None,  # optional
+        **opts,
     ):
         """
         GxB_Vector_import_xxx
@@ -664,6 +692,7 @@ def import_any(
             size=size,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             dtype=dtype,
             name=name,
@@ -675,6 +704,7 @@ def import_any(
             # Bitmap/Sparse
             nvals=nvals,
             method="import",
+            opts=opts,
         )
 
     def pack_any(
@@ -684,6 +714,7 @@ def pack_any(
         values,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
         # Sparse
         indices=None,
@@ -692,10 +723,11 @@ def pack_any(
         bitmap=None,
         # Bitmap/Sparse
         nvals=None,  # optional
-        # Unused for pack
+        # Unused for pack, ignored
         size=None,
         dtype=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Vector_pack_xxx
@@ -709,6 +741,7 @@ def pack_any(
             values=values,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             # Sparse
             indices=indices,
@@ -719,6 +752,7 @@ def pack_any(
             nvals=nvals,
             method="pack",
             vector=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -730,6 +764,7 @@ def _import_any(
         size=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
         dtype=None,
         name=None,
@@ -742,6 +777,7 @@ def _import_any(
         nvals=None,  # optional
         method,
         vector=None,
+        opts,
     ):
         if format is None:
             if indices is not None:
@@ -767,8 +803,10 @@ def _import_any(
                 is_iso=is_iso,
                 sorted_index=sorted_index,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "bitmap":
             return getattr(obj, f"{method}_bitmap")(
@@ -778,8 +816,10 @@ def _import_any(
                 size=size,
                 is_iso=is_iso,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         if format == "full":
             return getattr(obj, f"{method}_full")(
@@ -787,8 +827,10 @@ def _import_any(
                 size=size,
                 is_iso=is_iso,
                 take_ownership=take_ownership,
+                secure_import=secure_import,
                 dtype=dtype,
                 name=name,
+                **opts,
             )
         raise ValueError(f"Invalid format: {format}")
 
@@ -803,9 +845,11 @@ def import_sparse(
         is_iso=False,
         sorted_index=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Vector_import_CSC
@@ -858,10 +902,12 @@ def import_sparse(
             is_iso=is_iso,
             sorted_index=sorted_index,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_sparse(
@@ -873,8 +919,13 @@ def pack_sparse(
         is_iso=False,
         sorted_index=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **ignored_kwargs,
+        # Unused for pack, ignored
+        size=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Vector_pack_CSC
@@ -891,9 +942,11 @@ def pack_sparse(
             is_iso=is_iso,
             sorted_index=sorted_index,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             vector=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -907,11 +960,13 @@ def _import_sparse(
         is_iso=False,
         sorted_index=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         vector=None,
+        opts,
     ):
         if format is not None and format.lower() != "sparse":
             raise ValueError(f"Invalid format: {format!r}.  Must be None or 'sparse'.")
@@ -937,7 +992,8 @@ def _import_sparse(
         else:
             vhandle = vector._carg
             args = ()
-        status = libget(f"GxB_Vector_{method}_CSC")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Vector_{method}_CSC")(
             vhandle,
             *args,
             vi,
@@ -947,7 +1003,7 @@ def _import_sparse(
             is_iso,
             nvals,
             not sorted_index,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -972,9 +1028,11 @@ def import_bitmap(
         size=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Vector_import_Bitmap
@@ -1026,10 +1084,12 @@ def import_bitmap(
             size=size,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_bitmap(
@@ -1040,8 +1100,13 @@ def pack_bitmap(
         nvals=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        size=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Vector_pack_Bitmap
@@ -1057,9 +1122,11 @@ def pack_bitmap(
             nvals=nvals,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             vector=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -1072,11 +1139,13 @@ def _import_bitmap(
         size=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         vector=None,
+        opts,
     ):
         if format is not None and format.lower() != "bitmap":
             raise ValueError(f"Invalid format: {format!r}.  Must be None or 'bitmap'.")
@@ -1109,7 +1178,8 @@ def _import_bitmap(
         else:
             vhandle = vector._carg
             args = ()
-        status = libget(f"GxB_Vector_{method}_Bitmap")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Vector_{method}_Bitmap")(
             vhandle,
             *args,
             vb,
@@ -1118,7 +1188,7 @@ def _import_bitmap(
             values.nbytes,
             is_iso,
             nvals,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -1141,9 +1211,11 @@ def import_full(
         size=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
+        **opts,
     ):
         """
         GxB_Vector_import_Full
@@ -1189,10 +1261,12 @@ def import_full(
             size=size,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             dtype=dtype,
             format=format,
             name=name,
             method="import",
+            opts=opts,
         )
 
     def pack_full(
@@ -1201,8 +1275,13 @@ def pack_full(
         *,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         format=None,
-        **unused_kwargs,
+        # Unused for pack, ignored
+        size=None,
+        dtype=None,
+        name=None,
+        **opts,
     ):
         """
         GxB_Vector_pack_Full
@@ -1216,9 +1295,11 @@ def pack_full(
             values=values,
             is_iso=is_iso,
             take_ownership=take_ownership,
+            secure_import=secure_import,
             format=format,
             method="pack",
             vector=self._parent,
+            opts=opts,
         )
 
     @classmethod
@@ -1229,11 +1310,13 @@ def _import_full(
         size=None,
         is_iso=False,
         take_ownership=False,
+        secure_import=False,
         dtype=None,
         format=None,
         name=None,
         method,
         vector=None,
+        opts,
     ):
         if format is not None and format.lower() != "full":
             raise ValueError(f"Invalid format: {format!r}.  Must be None or 'full'.")
@@ -1255,13 +1338,14 @@ def _import_full(
         else:
             vhandle = vector._carg
             args = ()
-        status = libget(f"GxB_Vector_{method}_Full")(
+        desc = get_descriptor(secure_import=secure_import, **opts)
+        status = getattr(lib, f"GxB_Vector_{method}_Full")(
             vhandle,
             *args,
             vx,
             values.nbytes,
             is_iso,
-            NULL,
+            NULL if desc is None else desc._carg,
         )
         if method == "import":
             check_status_carg(
@@ -1279,7 +1363,7 @@ def _import_full(
     def head(self, n=10, dtype=None, *, sort=False):
         return head(self._parent, n, dtype, sort=sort)
 
-    def scan(self, op=monoid.plus, *, name=None):
+    def scan(self, op=monoid.plus, *, name=None, **opts):
         """Perform a prefix scan with the given monoid.
 
         For example, use `monoid.plus` (the default) to perform a cumulative sum,
@@ -1289,9 +1373,9 @@ def scan(self, op=monoid.plus, *, name=None):
         -------
         Scalar
         """
-        return prefix_scan(self._parent, op, name=name, within="scan")
+        return prefix_scan(self._parent, op, name=name, within="scan", **opts)
 
-    def reshape(self, nrows, ncols=None, order="rowwise", *, name=None):
+    def reshape(self, nrows, ncols=None, order="rowwise", *, name=None, **opts):
         """Return a copy of the Vector as a Matrix of the given shape.
 
         The shape of the Matrix must be compatible with the original shape.
@@ -1319,7 +1403,7 @@ def reshape(self, nrows, ncols=None, order="rowwise", *, name=None):
         --------
         Matrix.ss.flatten : flatten a Matrix into a Vector.
         """
-        return self._parent._as_matrix().ss.reshape(nrows, ncols, order, name=name)
+        return self._parent._as_matrix().ss.reshape(nrows, ncols, order, name=name, **opts)
 
     def selectk(self, how, k, *, name=None):
         """Select (up to) k elements.
@@ -1476,7 +1560,7 @@ def compactify(self, how="first", size=None, *, reverse=False, asindex=False, na
             name=name,
         )
 
-    def sort(self, op=binary.lt, *, values=True, permutation=True, nthreads=None):
+    def sort(self, op=binary.lt, *, values=True, permutation=True, **opts):
         """GxB_Vector_sort to sort values of the Vector
 
         Sorting moves all the elements to the left just like `compactify`.
@@ -1524,23 +1608,20 @@ def sort(self, op=binary.lt, *, values=True, permutation=True, nthreads=None):
             p = Vector(UINT64, parent._size, name="permutation")
         else:
             p = None
-        if nthreads is not None:
-            desc = get_nthreads_descriptor(nthreads)
-        else:
-            desc = None
+        desc = get_descriptor(**opts)
         check_status(
             lib.GxB_Vector_sort(
-                w._carg if w is not None else NULL,
-                p._carg if p is not None else NULL,
+                NULL if w is None else w._carg,
+                NULL if p is None else p._carg,
                 op._carg,
                 parent._carg,
-                desc._carg if desc is not None else NULL,
+                NULL if desc is None else desc._carg,
             ),
             parent,
         )
         return w, p
 
-    def serialize(self, compression="default", level=None, *, nthreads=None):
+    def serialize(self, compression="default", level=None, **opts):
         """Serialize a Vector to bytes (as numpy array) using SuiteSparse GxB_Vector_serialize.
 
         Parameters
@@ -1569,7 +1650,7 @@ def serialize(self, compression="default", level=None, *, nthreads=None):
 
         *Warning*: Behavior of serializing UDTs is experimental and may change in a future release.
         """
-        desc = get_compression_descriptor(compression, level=level, nthreads=nthreads)
+        desc = get_descriptor(compression=compression, compression_level=level, **opts)
         blob_handle = ffi_new("void**")
         blob_size_handle = ffi_new("GrB_Index*")
         parent = self._parent
@@ -1578,14 +1659,14 @@ def serialize(self, compression="default", level=None, *, nthreads=None):
                 blob_handle,
                 blob_size_handle,
                 parent._carg,
-                desc._carg,
+                NULL if desc is None else desc._carg,
             ),
             parent,
         )
         return claim_buffer(ffi, blob_handle[0], blob_size_handle[0], np.dtype(np.uint8))
 
     @classmethod
-    def deserialize(cls, data, dtype=None, *, nthreads=None, name=None):
+    def deserialize(cls, data, dtype=None, *, name=None, **opts):
         """Deserialize a Vector from bytes, buffer, or numpy array using GxB_Vector_deserialize.
 
         The data should have been previously serialized with a compatible version of
@@ -1627,13 +1708,12 @@ def deserialize(cls, data, dtype=None, *, nthreads=None, name=None):
             dtype = _string_to_dtype(dtype_name)
         else:
             dtype = lookup_dtype(dtype)
-        if nthreads is not None:
-            desc_obj = get_nthreads_descriptor(nthreads)._carg
-        else:
-            desc_obj = NULL
+        desc = get_descriptor(**opts)
         gb_obj = ffi_new("GrB_Vector*")
         check_status_carg(
-            lib.GxB_Vector_deserialize(gb_obj, dtype._carg, data_obj, data.nbytes, desc_obj),
+            lib.GxB_Vector_deserialize(
+                gb_obj, dtype._carg, data_obj, data.nbytes, NULL if desc is None else desc._carg
+            ),
             "Vector",
             gb_obj[0],
         )
diff --git a/graphblas/core/vector.py b/graphblas/core/vector.py
index 91f1a6c6a..ea343c50b 100644
--- a/graphblas/core/vector.py
+++ b/graphblas/core/vector.py
@@ -8,6 +8,7 @@
 from ..exceptions import DimensionMismatch, NoValue, check_status
 from . import automethods, ffi, lib, utils
 from .base import BaseExpression, BaseType, _check_mask, call
+from .descriptor import lookup as descriptor_lookup
 from .expr import _ALL_INDICES, AmbiguousAssignOrExtract, IndexerResolver, Updater
 from .mask import Mask, StructuralMask, ValueMask
 from .operator import UNKNOWN_OPCLASS, find_opclass, get_semiring, get_typed_op, op_from_string
@@ -39,8 +40,10 @@
 # Custom recipes
 def _v_add_m(updater, left, right, op):
     full = Vector(left.dtype, right._ncols, name="v_full")
-    full[:] = 0
-    temp = left.outer(full, binary.first).new(name="M_temp", mask=updater.kwargs.get("mask"))
+    full(**updater.opts)[:] = 0
+    temp = left.outer(full, binary.first).new(
+        name="M_temp", mask=updater.kwargs.get("mask"), **updater.opts
+    )
     updater << temp.ewise_add(right, op)
 
 
@@ -50,19 +53,22 @@ def _v_mult_m(updater, left, right, op):
 
 def _v_union_m(updater, left, right, left_default, right_default, op):
     full = Vector(left.dtype, right._ncols, name="v_full")
-    full[:] = 0
-    temp = left.outer(full, binary.first).new(name="M_temp", mask=updater.kwargs.get("mask"))
+    full(**updater.opts)[:] = 0
+    temp = left.outer(full, binary.first).new(
+        name="M_temp", mask=updater.kwargs.get("mask"), **updater.opts
+    )
     updater << temp.ewise_union(right, op, left_default=left_default, right_default=right_default)
 
 
 def _v_union_v(updater, left, right, left_default, right_default, op, dtype):
     mask = updater.kwargs.get("mask")
+    opts = updater.opts
     new_left = left.dup(dtype, clear=True)
-    new_left(mask=mask) << binary.second(right, left_default)
-    new_left(mask=mask) << binary.first(left | new_left)
+    new_left(mask=mask, **opts) << binary.second(right, left_default)
+    new_left(mask=mask, **opts) << binary.first(left | new_left)
     new_right = right.dup(dtype, clear=True)
-    new_right(mask=mask) << binary.second(left, right_default)
-    new_right(mask=mask) << binary.first(right | new_right)
+    new_right(mask=mask, **opts) << binary.second(left, right_default)
+    new_right(mask=mask, **opts) << binary.first(right | new_right)
     updater << op(new_left & new_right)
 
 
@@ -207,7 +213,7 @@ def V(self):
         """Create a Mask based on the values in the Vector (treating each value as truthy)."""
         return ValueMask(self)
 
-    def __delitem__(self, keys):
+    def __delitem__(self, keys, **opts):
         """Delete a single element or subvector.
 
         Examples
@@ -215,7 +221,7 @@ def __delitem__(self, keys):
 
             >>> del v[1:-1]
         """
-        del Updater(self)[keys]
+        del Updater(self, opts=opts)[keys]
 
     def __getitem__(self, keys):
         """Extract a single element or subvector.
@@ -236,7 +242,7 @@ def __getitem__(self, keys):
             return ScalarIndexExpr(self, resolved_indexes)
         return VectorIndexExpr(self, resolved_indexes, *shape)
 
-    def __setitem__(self, keys, expr):
+    def __setitem__(self, keys, expr, **opts):
         """Assign values to a single element or subvector.
 
         See the `Assign section <../user_guide/operations.html#assign>`__
@@ -250,7 +256,7 @@ def __setitem__(self, keys, expr):
             # This makes a dense iso-value vector
             v[:] = 1
         """
-        Updater(self)[keys] = expr
+        Updater(self, opts=opts)[keys] = expr
 
     def __contains__(self, index):
         """Indicates whether a value is present at the index.
@@ -284,7 +290,7 @@ def __sizeof__(self):
             return size[0] + object.__sizeof__(self)
         raise TypeError("Unable to get size of Vector with backend: {backend}")
 
-    def isequal(self, other, *, check_dtype=False):
+    def isequal(self, other, *, check_dtype=False, **opts):
         """Check for exact equality (same size, same structure)
 
         Parameters
@@ -316,15 +322,15 @@ def isequal(self, other, *, check_dtype=False):
             op = get_typed_op(binary.eq, self.dtype, other.dtype, kind="binary")
 
         matches = Vector(bool, self._size, name="v_isequal")
-        matches << self.ewise_mult(other, op)
+        matches(**opts) << self.ewise_mult(other, op)
         # ewise_mult performs intersection, so nvals will indicate mismatched empty values
         if matches._nvals != self._nvals:
             return False
 
         # Check if all results are True
-        return matches.reduce(monoid.land, allow_empty=False).new().value
+        return matches.reduce(monoid.land, allow_empty=False).new(**opts).value
 
-    def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False):
+    def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False, **opts):
         """Check for approximate equality (including same size and same structure).
 
         Equivalent to: ``abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)``.
@@ -353,14 +359,14 @@ def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False):
             return False
 
         matches = self.ewise_mult(other, binary.isclose(rel_tol, abs_tol)).new(
-            bool, name="M_isclose"
+            bool, name="M_isclose", **opts
         )
         # ewise_mult performs intersection, so nvals will indicate mismatched empty values
         if matches._nvals != self._nvals:
             return False
 
         # Check if all results are True
-        return matches.reduce(monoid.land, allow_empty=False).new().value
+        return matches.reduce(monoid.land, allow_empty=False).new(**opts).value
 
     @property
     def size(self):
@@ -535,7 +541,7 @@ def build(self, indices, values, *, dup_op=None, clear=False, size=None):
         if not dup_op_given and self._nvals < n:
             raise ValueError("Duplicate indices found, must provide `dup_op` BinaryOp")
 
-    def dup(self, dtype=None, *, clear=False, mask=None, name=None):
+    def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts):
         """Create a duplicate of the Vector.
 
         This is a full copy, not a view on the original.
@@ -560,8 +566,11 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None):
                 dtype = self.dtype
             rv = Vector(dtype, size=self._size, name=name)
             if not clear:
-                rv(mask=mask)[...] = self
+                rv(mask=mask, **opts)[...] = self
         else:
+            if opts:
+                # Ignore opts for now
+                descriptor_lookup(**opts)
             rv = Vector._from_obj(ffi_new("GrB_Vector*"), self.dtype, self._size, name=name)
             call("GrB_Vector_dup", [_Pointer(rv), self])
         return rv
@@ -1510,7 +1519,9 @@ def reposition(self, offset, *, size=None):
     ##################################
     # Extract and Assign index methods
     ##################################
-    def _extract_element(self, resolved_indexes, dtype=None, *, is_cscalar, name=None, result=None):
+    def _extract_element(
+        self, resolved_indexes, dtype, opts, *, is_cscalar, name=None, result=None
+    ):
         if dtype is None:
             dtype = self.dtype
         else:
@@ -1518,6 +1529,9 @@ def _extract_element(self, resolved_indexes, dtype=None, *, is_cscalar, name=Non
         idx = resolved_indexes.indices[0]
         if result is None:
             result = Scalar(dtype, is_cscalar=is_cscalar, name=name)
+        if opts:
+            # Ignore opts for now
+            descriptor_lookup(**opts)
         if is_cscalar:
             dtype_name = "UDT" if dtype._is_udt else dtype.name
             if (
@@ -1568,7 +1582,7 @@ def _assign_element(self, resolved_indexes, value):
             cfunc_name = "GrB_Vector_setElement_Scalar"
         call(cfunc_name, [self, value, idx.index])
 
-    def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False, replace=False):
+    def _prep_for_assign(self, resolved_indexes, value, mask, is_submask, replace, opts):
         method_name = "__setitem__"
         idx = resolved_indexes.indices[0]
         size = idx.size
@@ -1591,7 +1605,7 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                     cfunc_name = "GxB_Vector_subassign"
                 else:
                     cfunc_name = "GrB_Vector_assign"
-                    mask = _vanilla_subassign_mask(self, mask, idx, replace)
+                    mask = _vanilla_subassign_mask(self, mask, idx, replace, opts)
                 expr_repr = "[[{2._expr_name} elements]](%s) = {0.name}" % mask.name
             else:
                 # v(m)[I] << w
@@ -1648,9 +1662,10 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                             return self._prep_for_assign(
                                 resolved_indexes,
                                 vals,
-                                mask=mask,
-                                is_submask=is_submask,
-                                replace=False,
+                                mask,
+                                is_submask,
+                                replace,
+                                opts,
                             )
                     else:
                         extra_message = "Literal scalars also accepted."
@@ -1676,13 +1691,13 @@ def _prep_for_assign(self, resolved_indexes, value, mask=None, is_submask=False,
                         cfunc_name = f"GxB_Vector_subassign_{dtype_name}"
                     else:
                         cfunc_name = f"GrB_Vector_assign_{dtype_name}"
-                        mask = _vanilla_subassign_mask(self, mask, idx, replace)
+                        mask = _vanilla_subassign_mask(self, mask, idx, replace, opts)
                 else:
                     if backend == "suitesparse":
                         cfunc_name = "GxB_Vector_subassign_Scalar"
                     else:
                         cfunc_name = "GrB_Vector_assign_Scalar"
-                        mask = _vanilla_subassign_mask(self, mask, idx, replace)
+                        mask = _vanilla_subassign_mask(self, mask, idx, replace, opts)
                 expr_repr = "[[{2._expr_name} elements]](%s) = {0._expr_name}" % mask.name
             else:
                 # v(m)[I] << c
@@ -1766,18 +1781,18 @@ def to_dict(self):
     )
 
 
-def _vanilla_subassign_mask(self, mask, indices, replace):
+def _vanilla_subassign_mask(self, mask, indices, replace, opts):
     _check_mask(mask, self)
     if not replace and indices.index is _ALL_INDICES:
         return mask
     indices = indices._py_index()
     val = Vector(mask.parent.dtype, size=self._size, name="v_temp")
-    val[indices] = mask.parent
+    val(**opts)[indices] = mask.parent
     mask = type(mask)(val)
     if replace:
         val = self.dup()
-        del val[indices]
-        mask |= val.S
+        val.__delitem__(indices, **opts)  # del val[indices]
+        mask = mask.__or__(val.S, **opts)  # mask |= val.S
     return mask
 
 
diff --git a/graphblas/monoid/numpy.py b/graphblas/monoid/numpy.py
index 6672c41da..788b984b9 100644
--- a/graphblas/monoid/numpy.py
+++ b/graphblas/monoid/numpy.py
@@ -84,7 +84,11 @@
     _monoid_identities["minimum"].update(dict.fromkeys(_complex_dtypes, complex(_np.inf, _np.inf)))
 
 # To increase import speed, only call njit when `_config.get("mapnumpy")` is False
-if _config.get("mapnumpy") or type(_numba.njit(lambda x, y: _np.fmax(x, y))(1, 2)) is not float:
+if (
+    _config.get("mapnumpy")
+    or type(_numba.njit(lambda x, y: _np.fmax(x, y))(1, 2))  # pragma: no branch (numba)
+    is not float
+):
     # Incorrect behavior was introduced in numba 0.56.2 and numpy 1.23
     # See: https://github.com/numba/numba/issues/8478
     _monoid_identities["fmax"].update(
diff --git a/graphblas/ss/_core.py b/graphblas/ss/_core.py
index 5c74ab887..11001e127 100644
--- a/graphblas/ss/_core.py
+++ b/graphblas/ss/_core.py
@@ -21,7 +21,7 @@ class _graphblas_ss:
 _graphblas_ss = _graphblas_ss()
 
 
-def diag(x, k=0, dtype=None, *, name=None):
+def diag(x, k=0, dtype=None, *, name=None, **opts):
     """
     GxB_Matrix_diag, GxB_Vector_diag
 
@@ -66,11 +66,11 @@ def diag(x, k=0, dtype=None, *, name=None):
         if size < 0:
             size = 0
         rv = Vector(dtype, size=size, name=name)
-        rv.ss.build_diag(x, k)
+        rv.ss.build_diag(x, k, **opts)
     return rv
 
 
-def concat(tiles, dtype=None, *, name=None):
+def concat(tiles, dtype=None, *, name=None, **opts):
     """
     GxB_Matrix_concat
 
@@ -97,13 +97,13 @@ def concat(tiles, dtype=None, *, name=None):
         nrows = sum(row_tiles[0]._nrows for row_tiles in tiles)
         ncols = sum(tile._ncols for tile in tiles[0])
         rv = Matrix(dtype, nrows=nrows, ncols=ncols, name=name)
-        rv.ss._concat(tiles, m, n)
+        rv.ss._concat(tiles, m, n, opts)
     else:
         if dtype is None:
             dtype = tiles[0].dtype
         size = sum(tile._nrows for tile in tiles)
         rv = Vector(dtype, size=size, name=name)
-        rv.ss._concat(tiles, m)
+        rv.ss._concat(tiles, m, opts)
     return rv
 
 
@@ -164,13 +164,13 @@ class GlobalConfig(BaseConfig):
     }
     _enumerations = {
         "format": {
-            lib.GxB_BY_ROW: "by_row",
-            lib.GxB_BY_COL: "by_col",
-            # lib.GxB_NO_FORMAT: "no_format",  # Used by iterators; not valid here
+            "by_row": lib.GxB_BY_ROW,
+            "by_col": lib.GxB_BY_COL,
+            # "no_format": lib.GxB_NO_FORMAT,  # Used by iterators; not valid here
         },
         "gpu_control": {
-            lib.GxB_GPU_ALWAYS: "always",
-            lib.GxB_GPU_NEVER: "never",
+            "always": lib.GxB_GPU_ALWAYS,
+            "never": lib.GxB_GPU_NEVER,
         },
     }
 


Test patch:
diff --git a/graphblas/tests/conftest.py b/graphblas/tests/conftest.py
index ad7947b60..f5bf5257e 100644
--- a/graphblas/tests/conftest.py
+++ b/graphblas/tests/conftest.py
@@ -85,7 +85,7 @@ def reset_name_counters():
 
 
 @pytest.fixture(scope="session", autouse=True)
-def ic():
+def ic():  # pragma: no cover (debug)
     """Make `ic` available everywhere during testing for easier debugging"""
     try:
         import icecream
diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py
index ef973e25e..f474e3a53 100644
--- a/graphblas/tests/test_matrix.py
+++ b/graphblas/tests/test_matrix.py
@@ -537,7 +537,7 @@ def test_extract_input_mask():
         TypeError, match="Got Vector `input_mask` when extracting a submatrix from a Matrix"
     ):
         A(input_mask=expected.S) << A[[0], [0]]
-    with pytest.raises(TypeError, match="unexpected keyword argument"):
+    with pytest.raises(ValueError, match="input_mask"):
         A[0, 0].new(input_mask=M.S)
     with pytest.raises(TypeError, match="mask and input_mask arguments cannot both be given"):
         A[0, [0, 1]].new(input_mask=M.S, mask=expected.S)
@@ -1397,7 +1397,7 @@ def test_reduce_agg(A):
     silly = agg.Aggregator(
         "silly",
         composite=[agg.varp, agg.stdp],
-        finalize=lambda x, y: binary.times(x & y),
+        finalize=lambda x, y, opts: binary.times(x & y),
         types=[agg.varp],
     )
     v1 = A.reduce_rowwise(agg.varp).new()
@@ -1454,7 +1454,7 @@ def test_reduce_agg_argminmax(A):
     silly = agg.Aggregator(
         "silly",
         composite=[agg.argmin, agg.argmax],
-        finalize=lambda x, y: binary.plus(x & y),
+        finalize=lambda x, y, opts: binary.plus(x & y),
         types=[agg.argmin],
     )
     v1 = A.reduce_rowwise(agg.argmin).new()
@@ -1512,7 +1512,7 @@ def test_reduce_agg_firstlast(A):
     silly = agg.Aggregator(
         "silly",
         composite=[agg.first, agg.last],
-        finalize=lambda x, y: binary.plus(x & y),
+        finalize=lambda x, y, opts: binary.plus(x & y),
         types=[agg.first],
     )
     v1 = A.reduce_rowwise(agg.first).new()
@@ -1560,7 +1560,7 @@ def test_reduce_agg_firstlast_index(A):
     silly = agg.Aggregator(
         "silly",
         composite=[agg.first_index, agg.last_index],
-        finalize=lambda x, y: binary.plus(x & y),
+        finalize=lambda x, y, opts: binary.plus(x & y),
         types=[agg.first_index],
     )
     v1 = A.reduce_rowwise(agg.first_index).new()
@@ -4096,3 +4096,44 @@ def test_ss_sort(A):
     C, P = A.ss.sort(binary.gt, order="col")
     assert P.isequal(expected_P)
     assert C.isequal(expected_C)
+    C, P = A.ss.sort(monoid.lxnor)  # Weird, but user-defined monoids may not commute, so okay
+
+
+@autocompute
+def test_ss_descriptors(A):
+    if suitesparse:
+        C1 = (A @ A).new()
+        C2 = (A @ A).new(nthreads=4, axb_method="dot", sort=True)
+        assert C1.isequal(C2)
+        C2 = (A @ A).new(Nthreads=4, AxB_method="dot", sort=True)
+        assert C1.isequal(C2)
+        A(nthreads=4, axb_method="dot", sort=True) << A @ A
+        assert A.isequal(C2)
+        with pytest.raises(ValueError, match="escriptor"):
+            C1(bad_opt=True) << A
+        with pytest.raises(ValueError, match="Duplicate descriptor"):
+            (A @ A).new(nthreads=4, Nthreads=5)
+
+        with pytest.raises(ValueError, match="escriptor"):
+            A[0, 0].new(bad_opt=True)
+        A[0, 0].new(nthreads=4)  # ignored, but okay
+        with pytest.raises(ValueError, match="escriptor"):
+            A.__setitem__((0, 0), 1, bad_opt=True)
+        A.__setitem__((0, 0), 1, nthreads=4)  # ignored, but okay
+        with pytest.raises(ValueError, match="escriptor"):
+            A.dup(bad_opt=True)
+        A.dup(nthreads=4)
+        # These are interesting cases: we auto-compute a value, then provide custom descriptor
+        expr = A.reduce_scalar()
+        expr.value
+        with pytest.raises(ValueError, match="escriptor"):
+            expr.new(bad_opt=True)
+        expr.new(nthreads=4)  # ignored, but okay
+        expr = A.reduce_rowwise()
+        expr[0].value
+        with pytest.raises(ValueError, match="escriptor"):
+            expr.new(bad_opt=True)
+        expr.new(nthreads=4)  # ignored, but okay
+    else:
+        with pytest.raises(ValueError, match="escriptor"):
+            (A @ A).new(nthreads=4, axb_method="dot", sort=True)
diff --git a/graphblas/tests/test_op.py b/graphblas/tests/test_op.py
index 1bfd5ec7d..08beb86fa 100644
--- a/graphblas/tests/test_op.py
+++ b/graphblas/tests/test_op.py
@@ -272,9 +272,9 @@ def inner(left, right):
 
     # signatures must match
     with pytest.raises(ValueError, match="Signatures"):
-        Monoid.register_anonymous(bin_op, lambda x: -x)  # pragma: no cover (numba)
+        Monoid.register_anonymous(bin_op, lambda x: -x)  # pragma: no branch (numba)
     with pytest.raises(ValueError, match="Signatures"):
-        Monoid.register_anonymous(bin_op, lambda y=0: -y)  # pragma: no cover (numba)
+        Monoid.register_anonymous(bin_op, lambda y=0: -y)  # pragma: no branch (numba)
     with pytest.raises(TypeError, match="binaryop must be parameterized"):
         operator.ParameterizedMonoid("bad_monoid", binary.plus, 0)
 
diff --git a/graphblas/tests/test_resolving.py b/graphblas/tests/test_resolving.py
index 1ba4608c0..051140a31 100644
--- a/graphblas/tests/test_resolving.py
+++ b/graphblas/tests/test_resolving.py
@@ -170,7 +170,7 @@ def test_bad_extract_with_updater():
         u << u(mask=u.S)[[1, 2]]
     with pytest.raises(TypeError, match="Assignment value must be a valid expression"):
         u << u()[[1, 2]]
-    with pytest.raises(TypeError, match="unexpected keyword argument"):
+    with pytest.raises(ValueError, match="escriptor"):
         u[0].new(mask=u.S)
     s = Scalar.from_value(10)
     with pytest.raises(TypeError, match="Indexing not supported for Scalars"):
diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py
index 4f104dbb0..87e2b32b2 100644
--- a/graphblas/tests/test_scalar.py
+++ b/graphblas/tests/test_scalar.py
@@ -477,3 +477,25 @@ def test_get(s):
     s.clear()
     assert compute(s.get()) is None
     assert s.get("mittens") == "mittens"
+
+
+def test_ss_descriptors(s):
+    v = Vector.from_coo([0, 2], [10, 20])
+    if suitesparse:
+        with pytest.raises(ValueError, match="escriptor"):
+            v[0].new(bad_opt=True)
+        assert v[0].new(nthreads=4) == 10  # ignored, but okay
+        with pytest.raises(ValueError, match="escriptor"):
+            v.dup(bad_opt=True)
+        v.dup(nthreads=4)
+        with pytest.raises(ValueError, match="escriptor"):
+            s(bad_opt=True) << 1
+        s(nthreads=4) << 1  # ignored, but okay
+        with pytest.raises(ValueError, match="escriptor"):
+            s(bad_opt=True) << s
+        s(nthreads=4) << s  # ignored, but okay
+    else:
+        with pytest.raises(ValueError, match="escriptor"):
+            v[0].new(nthreads=4)
+        with pytest.raises(ValueError, match="escriptor"):
+            s(nthreads=4) << 1
diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py
index cdc462eff..cd3e9595c 100644
--- a/graphblas/tests/test_vector.py
+++ b/graphblas/tests/test_vector.py
@@ -895,7 +895,7 @@ def test_reduce_agg(v):
     silly = agg.Aggregator(
         "silly",
         composite=[agg.varp, agg.stdp],
-        finalize=lambda x, y: binary.times(x & y),
+        finalize=lambda x, y, opts: binary.times(x & y),
         types=[agg.varp],
     )
     s = v.reduce(silly).new()
@@ -912,7 +912,7 @@ def test_reduce_agg_argminmax(v):
     silly = agg.Aggregator(
         "silly",
         composite=[agg.argmin, agg.argmax],
-        finalize=lambda x, y: binary.plus(x & y),
+        finalize=lambda x, y, opts: binary.plus(x & y),
         types=[agg.argmin],
     )
     s = v.reduce(silly).new()
@@ -930,7 +930,7 @@ def test_reduce_agg_firstlast(v):
     silly = agg.Aggregator(
         "silly",
         composite=[agg.first, agg.last],
-        finalize=lambda x, y: binary.plus(x & y),
+        finalize=lambda x, y, opts: binary.plus(x & y),
         types=[agg.first],
     )
     s = v.reduce(silly).new()
@@ -944,7 +944,7 @@ def test_reduce_agg_firstlast_index(v):
     silly = agg.Aggregator(
         "silly",
         composite=[agg.first_index, agg.last_index],
-        finalize=lambda x, y: binary.plus(x & y),
+        finalize=lambda x, y, opts: binary.plus(x & y),
         types=[agg.first_index],
     )
     s = v.reduce(silly).new()
@@ -2426,10 +2426,11 @@ def test_ss_sort(v):
         v.ss.sort(binary.plus)
 
     # Like compactify
-    _, p = v.ss.sort(lambda x, y: False, values=False)
+    _, p = v.ss.sort(lambda x, y: False, values=False)  # pragma: no branch (numba)
     expected_p = Vector.from_coo([0, 1, 2, 3], [1, 3, 4, 6], size=7)
     assert p.isequal(expected_p)
     # reversed
     _, p = v.ss.sort(binary.pair[bool], values=False)
     expected_p = Vector.from_coo([0, 1, 2, 3], [6, 4, 3, 1], size=7)
     assert p.isequal(expected_p)
+    w, p = v.ss.sort(monoid.lxor)  # Weird, but user-defined monoids may not commute, so okay