diff --git a/graphblas/core/ss/__init__.py b/graphblas/core/ss/__init__.py index 79859eb53..7ee1da469 100644 --- a/graphblas/core/ss/__init__.py +++ b/graphblas/core/ss/__init__.py @@ -1,5 +1,17 @@ import suitesparse_graphblas as _ssgb -version_major, version_minor, version_bug = map(int, _ssgb.__version__.split(".")[:3]) +# Version of the SuiteSparse:GraphBLAS C library, which is what every version +# gate in this package is really asking about. Read from the library's own +# constants rather than parsed out of ``_ssgb.__version__``: the wrapper's +# version normally tracks the library, but a development build between +# releases reports the previous one (10.4.1.0 against a 10.5.0 library), and a +# gate keyed on that takes the wrong branch without saying so. These are cffi +# ``#define`` constants, so they read at import time, before ``GrB_init``. +try: + version_major = _ssgb.lib.GxB_IMPLEMENTATION_MAJOR + version_minor = _ssgb.lib.GxB_IMPLEMENTATION_MINOR + version_bug = _ssgb.lib.GxB_IMPLEMENTATION_SUB +except AttributeError: # pragma: no cover (build without the GxB defines) + version_major, version_minor, version_bug = map(int, _ssgb.__version__.split(".")[:3]) _IS_SSGB7 = version_major == 7 diff --git a/graphblas/tests/test_formatting.py b/graphblas/tests/test_formatting.py index a6522dcef..2b412ac1d 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -5032,7 +5032,12 @@ def test_empty(): @pytest.mark.skipif("not pd") def test_vector_as_matrix(): - v = Vector.from_coo([1], [2], name="v_A") + # Built iso outright: the repr asserted below names the format, and + # SuiteSparse 10.5.0 no longer infers iso from a build whose values happen + # to be equal, so ``from_coo`` would render "bitmapc" on 10.5.0 and + # "bitmapc (iso)" on everything older. + v = Vector(int, 2, name="v_A") + v.ss.build_scalar([1], 2) A = v._as_matrix() repr_printer(A, "A") assert repr(A) == ( diff --git a/graphblas/tests/test_io.py b/graphblas/tests/test_io.py index a8d80e9ca..962595aaa 100644 --- a/graphblas/tests/test_io.py +++ b/graphblas/tests/test_io.py @@ -430,18 +430,28 @@ def test_awkward_roundtrip(): @pytest.mark.skipif("not ak") @pytest.mark.xfail(np.__version__[:5] in {"1.25.", "1.26."}, reason="awkward bug with numpy >=1.25") def test_awkward_iso_roundtrip(): + # Build iso outright rather than relying on all-equal values: SuiteSparse + # 10.5.0 no longer checks after a build whether every value is the same, + # so ``from_coo`` alone would leave these non-iso and the test would stop + # exercising the iso path it is named for. # Vector - v = gb.Vector.from_coo([1, 3, 5], [20, 20, 20], size=22) if suitesparse: + v = gb.Vector(int, 22) + v.ss.build_scalar([1, 3, 5], 20) assert v.ss.is_iso + else: + v = gb.Vector.from_coo([1, 3, 5], [20, 20, 20], size=22) kv = gb.io.to_awkward(v) assert isinstance(kv, ak.Array) v2 = gb.io.from_awkward(kv) assert v2.isequal(v) # Matrix - m = gb.Matrix.from_coo([0, 0, 3, 5], [1, 4, 0, 2], [1, 1, 1, 1], nrows=7, ncols=6) if suitesparse: + m = gb.Matrix(int, 7, 6) + m.ss.build_scalar([0, 0, 3, 5], [1, 4, 0, 2], 1) assert m.ss.is_iso + else: + m = gb.Matrix.from_coo([0, 0, 3, 5], [1, 4, 0, 2], [1, 1, 1, 1], nrows=7, ncols=6) for format in ["csr", "csc", "hypercsr", "hypercsc"]: km = gb.io.to_awkward(m, format=format) assert isinstance(km, ak.Array) diff --git a/graphblas/tests/test_ss_utils.py b/graphblas/tests/test_ss_utils.py index 40774186c..2c9e01a1a 100644 --- a/graphblas/tests/test_ss_utils.py +++ b/graphblas/tests/test_ss_utils.py @@ -10,6 +10,24 @@ pytest.skip("gb.ss and A.ss only available with suitesparse backend", allow_module_level=True) +# ``from_coo`` with all-equal values used to come back iso, because SuiteSparse +# checked after a build whether every value was the same. 10.5.0 dropped that +# check ("GrB_Matrix_build and GrB_Vector_build: no longer do a post-iso +# check"), so the iso arm of these tests has to ask for iso outright. +# ``ss.build_scalar`` does, and produces the same storage formats on every +# version, which is what the tests below are really selecting for. +def _iso_vector(indices, value, size): + v = Vector(int, size) + v.ss.build_scalar(indices, value) + return v + + +def _iso_matrix(rows, cols, value, nrows, ncols): + A = Matrix(int, nrows, ncols) + A.ss.build_scalar(rows, cols, value) + return A + + @pytest.mark.parametrize("do_iso", [False, True]) def test_vector_head(do_iso): v0 = Vector(int, 5) @@ -19,9 +37,14 @@ def test_vector_head(do_iso): values1 = [10, 20, 30] values2 = [2, 4, 6] values3 = [1, 2, 3] - v1 = Vector.from_coo([0, 1, 2], values1) # full - v2 = Vector.from_coo([1, 3, 5], values2) # bitmap - v3 = Vector.from_coo([100, 200, 300], values3) # sparse + if do_iso: + v1 = _iso_vector([0, 1, 2], values1[0], 3) # full + v2 = _iso_vector([1, 3, 5], values2[0], 6) # bitmap + v3 = _iso_vector([100, 200, 300], values3[0], 301) # sparse + else: + v1 = Vector.from_coo([0, 1, 2], values1) # full + v2 = Vector.from_coo([1, 3, 5], values2) # bitmap + v3 = Vector.from_coo([100, 200, 300], values3) # sparse assert v1.ss.export()["format"] == "full" assert v2.ss.export()["format"] == "bitmap" assert v3.ss.export()["format"] == "sparse" @@ -72,10 +95,16 @@ def test_matrix_head(do_iso): values2 = [1, 2, 4] values3 = values4 = [1, 2, 3] - A1 = Matrix.from_coo([0, 0, 1, 1], [0, 1, 0, 1], values1) # fullr - A2 = Matrix.from_coo([0, 0, 1], [0, 1, 1], values2) # Bitmap - A3 = Matrix.from_coo([5, 5, 10], [4, 5, 10], values3) # CSR - A4 = Matrix.from_coo([500, 500, 1000], [400, 500, 1000], values4) # HyperCSR + if do_iso: + A1 = _iso_matrix([0, 0, 1, 1], [0, 1, 0, 1], values1[0], 2, 2) # fullr + A2 = _iso_matrix([0, 0, 1], [0, 1, 1], values2[0], 2, 2) # Bitmap + A3 = _iso_matrix([5, 5, 10], [4, 5, 10], values3[0], 11, 11) # CSR + A4 = _iso_matrix([500, 500, 1000], [400, 500, 1000], values4[0], 1001, 1001) # HyperCSR + else: + A1 = Matrix.from_coo([0, 0, 1, 1], [0, 1, 0, 1], values1) # fullr + A2 = Matrix.from_coo([0, 0, 1], [0, 1, 1], values2) # Bitmap + A3 = Matrix.from_coo([5, 5, 10], [4, 5, 10], values3) # CSR + A4 = Matrix.from_coo([500, 500, 1000], [400, 500, 1000], values4) # HyperCSR d = A1.ss.export(raw=True) assert d["format"] == "fullr" d["format"] = "fullc" @@ -200,8 +229,19 @@ def test_about(): def test_openmp_enabled(): - # SuiteSparse:GraphBLAS without OpenMP enabled is very undesirable - assert gb.ss.about["openmp"] + # A SuiteSparse:GraphBLAS built without OpenMP is single-threaded, which is + # a large and entirely silent performance loss: nothing else about the + # library reports it, and the conda package still depends on an OpenMP + # runtime whether or not the build actually found one. Keep this assertion + # hard; it is how the conda-forge osx-arm64 regression in graphblas 10.5.0 + # was noticed. + assert gb.ss.about["openmp"], ( + "libgraphblas was built without OpenMP and will run single-threaded. " + "This is a defect in the installed build, not in python-graphblas. " + "Reinstall or rebuild graphblas with OpenMP enabled " + "(SuiteSparse only warns when its CMake cannot find OpenMP, so a " + "serial library can ship looking normal)." + ) def test_global_config():