diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 335c1f3e8..e1876c58f 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -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, @@ -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, diff --git a/graphblas/core/ss/matrix.py b/graphblas/core/ss/matrix.py index 9fc535924..443019557 100644 --- a/graphblas/core/ss/matrix.py +++ b/graphblas/core/ss/matrix.py @@ -17,6 +17,7 @@ _distinct_buffers, _MatrixArray, _Pointer, + check_values_size, get_order, get_shape, ints_to_numpy_buffer, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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 @@ -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 diff --git a/graphblas/core/ss/vector.py b/graphblas/core/ss/vector.py index 9cd4e22d0..8ee259510 100644 --- a/graphblas/core/ss/vector.py +++ b/graphblas/core/ss/vector.py @@ -16,6 +16,7 @@ _CArray, _distinct_buffers, _MatrixArray, + check_values_size, ints_to_numpy_buffer, normalize_chunks, values_to_numpy_buffer, @@ -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, @@ -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, @@ -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, diff --git a/graphblas/core/utils.py b/graphblas/core/utils.py index 7da0e0645..0b6b46a28 100644 --- a/graphblas/core/utils.py +++ b/graphblas/core/utils.py @@ -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: diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 30d2a5fa0..9d5d43f52 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -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) @@ -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): diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index de1d2a53d..3e4c9cf40 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -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