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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions graphblas/core/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1269,11 +1269,16 @@ def from_dcsr(

"""
if backend == "suitesparse":
values, dtype = values_to_numpy_buffer(values, dtype, subarray_after=1)
is_iso = values.ndim == 0
if is_iso:
values = np.broadcast_to(values, dtype.np_type.shape) # One whole element
return cls.ss.import_hypercsr(
rows=compressed_rows,
indptr=indptr,
col_indices=col_indices,
values=values,
is_iso=is_iso,
nrows=nrows,
ncols=ncols,
dtype=dtype,
Expand Down Expand Up @@ -1354,11 +1359,16 @@ def from_dcsc(

"""
if backend == "suitesparse":
values, dtype = values_to_numpy_buffer(values, dtype, subarray_after=1)
is_iso = values.ndim == 0
if is_iso:
values = np.broadcast_to(values, dtype.np_type.shape) # One whole element
return cls.ss.import_hypercsc(
cols=compressed_cols,
indptr=indptr,
row_indices=row_indices,
values=values,
is_iso=is_iso,
nrows=nrows,
ncols=ncols,
dtype=dtype,
Expand Down
11 changes: 11 additions & 0 deletions graphblas/core/ss/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
_distinct_buffers,
_MatrixArray,
_Pointer,
check_values_size,
get_order,
get_shape,
ints_to_numpy_buffer,
Expand Down Expand Up @@ -1353,6 +1354,8 @@ def _import_csr(
else:
mhandle = matrix._carg
args = ()
n = nrows if method == "import" else matrix._nrows
check_values_size(values, dtype, n, is_iso, indptr=indptr, indices=col_indices)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Matrix_{method}_CSR")(
mhandle,
Expand Down Expand Up @@ -1557,6 +1560,8 @@ def _import_csc(
else:
mhandle = matrix._carg
args = ()
n = ncols if method == "import" else matrix._ncols
check_values_size(values, dtype, n, is_iso, indptr=indptr, indices=row_indices)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Matrix_{method}_CSC")(
mhandle,
Expand Down Expand Up @@ -1793,6 +1798,7 @@ def _import_hypercsr(
else:
mhandle = matrix._carg
args = ()
check_values_size(values, dtype, nvec, is_iso, indptr=indptr, indices=col_indices)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Matrix_{method}_HyperCSR")(
mhandle,
Expand Down Expand Up @@ -2032,6 +2038,7 @@ def _import_hypercsc(
else:
mhandle = matrix._carg
args = ()
check_values_size(values, dtype, nvec, is_iso, indptr=indptr, indices=row_indices)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Matrix_{method}_HyperCSC")(
mhandle,
Expand Down Expand Up @@ -2244,6 +2251,7 @@ def _import_bitmapr(
else:
mhandle = matrix._carg
args = ()
check_values_size(values, dtype, nrows * ncols, is_iso)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Matrix_{method}_BitmapR")(
mhandle,
Expand Down Expand Up @@ -2451,6 +2459,7 @@ def _import_bitmapc(
else:
mhandle = matrix._carg
args = ()
check_values_size(values, dtype, nrows * ncols, is_iso)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Matrix_{method}_BitmapC")(
mhandle,
Expand Down Expand Up @@ -2632,6 +2641,7 @@ def _import_fullr(
else:
mhandle = matrix._carg
args = ()
check_values_size(values, dtype, nrows * ncols, is_iso)
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
Expand Down Expand Up @@ -2804,6 +2814,7 @@ def _import_fullc(
else:
mhandle = matrix._carg
args = ()
check_values_size(values, dtype, nrows * ncols, is_iso)
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
Expand Down
4 changes: 4 additions & 0 deletions graphblas/core/ss/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
_CArray,
_distinct_buffers,
_MatrixArray,
check_values_size,
ints_to_numpy_buffer,
normalize_chunks,
values_to_numpy_buffer,
Expand Down Expand Up @@ -1016,6 +1017,7 @@ def _import_sparse(
else:
vhandle = vector._carg
args = ()
check_values_size(values, dtype, nvals, is_iso)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Vector_{method}_CSC")(
vhandle,
Expand Down Expand Up @@ -1218,6 +1220,7 @@ def _import_bitmap(
else:
vhandle = vector._carg
args = ()
check_values_size(values, dtype, size, is_iso)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Vector_{method}_Bitmap")(
vhandle,
Expand Down Expand Up @@ -1393,6 +1396,7 @@ def _import_full(
else:
vhandle = vector._carg
args = ()
check_values_size(values, dtype, size, is_iso)
desc = get_descriptor(secure_import=secure_import, **opts)
status = getattr(lib, f"GxB_Vector_{method}_Full")(
vhandle,
Expand Down
28 changes: 28 additions & 0 deletions graphblas/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,34 @@ def check_element_shape(values, dtype, ndim):
)


def check_values_size(values, dtype, count, is_iso, *, indptr=None, indices=None):
"""Raise unless ``values`` holds ``count`` elements of ``dtype``, or one if ``is_iso``.

With ``indptr``, the count is ``indptr[count]``, as GraphBLAS reads it, and must not
exceed the number of ``indices``. GraphBLAS imports before 10.5.2 compare the byte
size of the values with the number of entries, not with the bytes those entries need,
so a short array gets through and is read past its end. 10.5.2 raises InvalidValue
with no message instead, so this check stays.
"""
if indptr is not None:
# GraphBLAS rejects an indptr this short itself
count = int(indptr[count]) if count < indptr.size else 0
if indices is not None and indices.size < count:
raise ValueError(
f"index pointers give {count} entries, more than the number of indices, "
f"{indices.size}"
)
if is_iso:
count = 1
if (have := values.nbytes // dtype.np_type.itemsize) < count:
if values.ndim == 0 and have == 0:
raise ValueError(
f"a scalar does not fill one element of dtype {dtype}, "
f"of shape {dtype.np_type.shape}"
)
raise ValueError(f"values must have {count} or more elements of dtype {dtype}, not {have}")


def normalize_values(self, values, dtype, shape=None, is_iso=False):
"""Expand and/or update dtype of values array."""
if dtype is not None:
Expand Down
73 changes: 73 additions & 0 deletions graphblas/tests/test_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,7 @@ def test_ss_import_export(A, do_iso, methods):
assert B5b.nvals == 2 * A.nvals
assert B5b.nrows == 2 * A.nrows
assert B5b.ncols == A.ncols
assert B5b[7:, :].new().isequal(A)
else:
A5.ss.pack_any(**d)
assert A5.isequal(A)
Expand Down Expand Up @@ -4533,11 +4534,83 @@ def test_subarray_dtype_values_shape():
assert A.to_coo()[2].tolist() == expected
A = Matrix.from_csr([0, 1, 2], [0, 1], 1.5, "FP64[2]")
assert A.to_coo()[2].tolist() == expected
# on both backends (SuiteSparse raised "values must have 1 or more elements ... not 0")
A = Matrix.from_dcsr([0, 1], [0, 1, 2], [0, 1], 1.5, "FP64[2]")
assert A.to_coo()[2].tolist() == expected
A = Matrix.from_dcsc([0, 1], [0, 1, 2], [0, 1], 1.5, "FP64[2]")
assert A.to_coo()[2].tolist() == expected
# The lengths compared are the number of elements, not of numbers (it said 6)
with pytest.raises(ValueError, match="lengths must match: 2, 2, 3$"):
Matrix.from_coo([0, 1], [0, 1], np.ones((3, 2)), "FP64[2]")


@pytest.mark.skipif("not suitesparse")
def test_ss_import_values_size():
# GraphBLAS before 10.5.2 compares the byte size of imported values with the number
# of entries, not the bytes they need, so it read past the end of each of these
match = "have 3 or more elements of dtype FP64, not 1"
with pytest.raises(ValueError, match=match):
Matrix.ss.import_csr(
nrows=2, ncols=2, indptr=[0, 1, 3], col_indices=[0, 0, 1], values=np.ones(1)
)
A = Matrix(float, 2, 2)
with pytest.raises(ValueError, match=match):
A.ss.pack_csc(indptr=[0, 1, 3], row_indices=[0, 0, 1], values=np.ones(1))
with pytest.raises(ValueError, match=match):
Matrix.ss.import_hypercsr(
nrows=3, ncols=3, rows=[0, 2], indptr=[0, 1, 3], col_indices=[0, 0, 1], values=[1.0]
)
with pytest.raises(ValueError, match="have 4 or more elements of dtype FP64, not 3"):
Matrix.ss.import_fullr(values=np.ones(3), nrows=2, ncols=2)
with pytest.raises(ValueError, match="have 4 or more elements of dtype FP64, not 2"):
Matrix.ss.import_bitmapc(bitmap=np.ones((2, 2), bool), values=np.ones(2), nrows=2, ncols=2)
# The dtype's name is whichever another test gave this anonymous dtype
match = r"have 2 or more elements of dtype \S+, not 1"
with pytest.raises(ValueError, match=match):
Matrix.ss.import_hypercsc(
nrows=3,
ncols=3,
cols=[0, 2],
indptr=[0, 1, 2],
row_indices=[0, 1],
values=np.ones(2),
dtype="FP64[2]",
)
with pytest.raises(ValueError, match=match):
Matrix.from_dcsr([0, 1], [0, 1, 2], [0, 1], np.ones(2), "FP64[2]")
with pytest.raises(ValueError, match=r"have 4 or more elements of dtype \S+, not 2"):
Matrix.ss.import_fullc(values=np.ones((2, 2, 1)), dtype="FP64[2]")
# A scalar is not a whole element of a sub-array dtype (it said "not 0")
with pytest.raises(ValueError, match=r"a scalar does not fill one element of dtype \S+, of"):
Matrix.ss.import_fullr(values=1.5, nrows=2, ncols=2, dtype="FP64[2]", is_iso=True)
# A scalar that is one whole element, where more are needed, is just too few values
with pytest.raises(ValueError, match=r"have 4 or more elements of dtype FP64, not 1"):
Matrix.ss.import_fullr(values=1.5, nrows=2, ncols=2)
# More entries than indices is an indptr problem; it was reported as too few values
match = "index pointers give 4611686018427387904 entries, more than the number of indices, 1"
with pytest.raises(ValueError, match=match):
Matrix.ss.import_csr(nrows=1, ncols=3, indptr=[0, 2**62], col_indices=[0], values=[1.0])
with pytest.raises(ValueError, match="index pointers give 3 entries, more than the number"):
Matrix.ss.import_hypercsc(
nrows=3, ncols=3, cols=[0, 2], indptr=[0, 1, 3], row_indices=[0, 1], values=[1.0]
)
# One element is enough for iso
A = Matrix.ss.import_fullr(values=[7.0], nrows=2, ncols=2, is_iso=True)
assert A.ss.is_iso
assert A.to_coo()[2].tolist() == [7.0] * 4
A = Matrix.ss.import_csr(
nrows=2,
ncols=2,
indptr=[0, 1, 2],
col_indices=[0, 1],
values=[1.0, 2.0],
dtype="FP64[2]",
is_iso=True,
)
assert A.ss.is_iso
assert A.to_coo()[2].tolist() == [[1.0, 2.0]] * 2


def test_power(A):
expected = A.dup()
for i in range(1, 50):
Expand Down
27 changes: 27 additions & 0 deletions graphblas/tests/test_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3109,3 +3109,30 @@ def test_subarray_dtype_values_shape():
# The lengths compared are the number of elements, not of numbers (it said 2 != 6)
with pytest.raises(ValueError, match="lengths must match: 2 != 3$"):
Vector.from_coo([0, 1], np.ones((3, 2)), "FP64[2]")


@pytest.mark.skipif("not suitesparse")
def test_ss_import_values_size():
# GraphBLAS before 10.5.2 compares the byte size of imported values with the number
# of entries, not the bytes they need, so it read past the end of each of these
with pytest.raises(ValueError, match="have 4 or more elements of dtype FP64, not 2"):
Vector.ss.import_sparse(size=5, indices=[0, 1, 2, 3], values=np.ones(2), nvals=4)
with pytest.raises(ValueError, match="have 5 or more elements of dtype FP64, not 3"):
Vector.ss.import_full(values=np.ones(3), size=5)
v = Vector(float, 5)
with pytest.raises(ValueError, match="have 5 or more elements of dtype FP64, not 3"):
v.ss.pack_full(values=np.ones(3))
# The dtype's name is whichever another test gave this anonymous dtype
with pytest.raises(ValueError, match=r"have 2 or more elements of dtype \S+, not 1"):
Vector.ss.import_sparse(size=3, indices=[0, 1], values=np.ones(2), dtype="FP64[2]")
with pytest.raises(ValueError, match=r"have 3 or more elements of dtype \S+, not 1"):
Vector.ss.import_bitmap(bitmap=[True, True, True], values=np.ones(3), dtype="FP64[2]")
# One element is enough for iso
v = Vector.ss.import_full(values=[4.0], size=5, is_iso=True)
assert v.ss.is_iso
assert v.to_coo()[1].tolist() == [4.0] * 5
v = Vector.ss.import_sparse(
size=3, indices=[0, 1], values=[2.0, 3.0], dtype="FP64[2]", is_iso=True
)
assert v.ss.is_iso
assert v.to_coo()[1].tolist() == [[2.0, 3.0]] * 2
Loading