From 18ae0f35458e414b4e66d68f52ec0ca6d61e1382 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 5 Aug 2026 01:50:53 -0500 Subject: [PATCH 1/5] Fix chop_threshold crashes in the pandas-free complex repr The COO form formats a complex column through _format_float_column, where display.chop_threshold compares each value's magnitude. Two crashes hid there, found by an overnight differential fuzz against the pandas renderer: - abs() of a python complex raises OverflowError for finite components near the float max, where numpy's abs returns inf. pandas therefore never chops such a value and renders the object fine; the pandas-free path crashed. Treat an overflowing magnitude as never chopped. Byte-identity for this case is restored, verified against the pandas renderer at 6f1eb02 (repr and HTML hashes equal). - A value that actually chops was replaced with float 0.0, which puts a j-less string into the complex column, and _trim_zeros_complex cannot parse that (IndexError). pandas 3.0.3 has the same bug in its own chop path (pandas.io.formats.format._trim_zeros_complex raises the same IndexError on the same input), so byte-identity here means identity with a crash. Chop to the value's own type of zero instead: the chopped cell renders 0.000000e+00+0.000000e+00j and the object reprs where pandas cannot. The second point is a deliberate, documented divergence from pandas, reachable only with display.chop_threshold set on a complex dtype. --- graphblas/core/formatting.py | 13 ++++++++++++- graphblas/tests/test_formatting.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/graphblas/core/formatting.py b/graphblas/core/formatting.py index 32be8a89a..f36ae04ea 100644 --- a/graphblas/core/formatting.py +++ b/graphblas/core/formatting.py @@ -487,7 +487,18 @@ def base(v): return base def formatter(v): - return base(v) if abs(v) > threshold else base(0.0) + try: + mag = abs(v) + except OverflowError: + # abs() of a python complex overflows for finite components near + # the float max, where numpy's abs returns inf. pandas computes the + # magnitude with numpy, so such a value is never chopped. + return base(v) + # Chop to the value's own type of zero. pandas chops at the array + # level, so a chopped complex renders " 0.000000+0.000000j"; a bare + # float zero here would put a j-less string into the complex column, + # which _trim_zeros_complex cannot parse. + return base(v) if mag > threshold else base(type(v)(0)) return formatter diff --git a/graphblas/tests/test_formatting.py b/graphblas/tests/test_formatting.py index 3da1297d4..ddc45b51e 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -6760,3 +6760,32 @@ def test_vector_as_matrix(): "\n" "" ) + + +@pytest.mark.skipif("not pd") +def test_chop_threshold_complex(): + # The COO (long) form formats complex columns through the float column + # formatter, where display.chop_threshold compares each value's magnitude. + big = float(np.finfo(np.float64).max) + v = Vector.from_coo( + [10_000, 500_000], [complex(big, big), complex(1e-7, 0)], size=10**6, dtype="FC64" + ) + with pd.option_context("display.chop_threshold", 1e-8): + # abs() of a python complex overflows for components near the float + # max; numpy's abs is inf, so nothing chops and this must render. + # Byte-identical to the pandas renderer, verified against 6f1eb02. + r = repr(v) + assert "1.797693e+308+1.797693e+308j" in r + assert "1.000000e-07+ 0.000000e+00j" in r + v._repr_html_() + # A value below the threshold chops to complex zero. pandas 3.0.3 + # itself crashes on this input (its own _trim_zeros_complex cannot + # parse the float zero its chop substitutes into a complex column), + # so rendering at all is deliberately better than byte-identity. + w = Vector.from_coo( + [10_000, 500_000], [complex(1.0, 1.0), complex(5e-9, 0)], size=10**6, dtype="FC64" + ) + r = repr(w) + assert "1.000000e+00+1.000000e+00j" in r + assert "0.000000e+00+0.000000e+00j" in r + w._repr_html_() From 448f6471438c63d1fa7d355c4c4ed68797826b4c Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 6 Aug 2026 00:37:10 -0700 Subject: [PATCH 2/5] Skip the complex chop_threshold test when FC64 is unavailable On Windows with python-suitesparse-graphblas older than 7.4.3.1 the CFFI bindings are built without complex support (MSVC lacks C99 _Complex), so dtypes._supports_complex is False, FC64 is never registered, and Vector.from_coo(..., dtype="FC64") raises ValueError before the test gets to the formatting code under test. Guard with the same skipif("not dtypes._supports_complex") pattern test_scalar.py uses; the module already imports dtypes, which the string condition needs. Verified the test still runs and passes on a complex-capable host, and that the skipif string evaluates to True in the module namespace when _supports_complex is flipped to False. --- graphblas/tests/test_formatting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphblas/tests/test_formatting.py b/graphblas/tests/test_formatting.py index ddc45b51e..a141906ce 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -6763,6 +6763,7 @@ def test_vector_as_matrix(): @pytest.mark.skipif("not pd") +@pytest.mark.skipif("not dtypes._supports_complex") def test_chop_threshold_complex(): # The COO (long) form formats complex columns through the float column # formatter, where display.chop_threshold compares each value's magnitude. From 4dd3bd8fcda5290bfc8377b9a4d3e163cd1e3c3b Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 24 Sep 2026 10:23:41 -0500 Subject: [PATCH 3/5] Compute the complex repr's magnitudes without numpy's overflow warning test_chop_threshold_complex failed on the macos-latest job that drew numpy 1.24.4: RuntimeWarning: overflow encountered in absolute The pandas-free float formatter takes np.abs of the column to choose between fixed and scientific notation. For a complex value with components near the float max the magnitude is inf, which those checks already handle, but numpy 1.24 on arm64 also raises the FP overflow flag computing it (1.26 and 2.4 do not), so the warning escaped from repr and the test suite's filterwarnings setting turned it into an error. Reproduced locally with numpy 1.24.4, pandas 2.2.3 and psg 8.2.0.1, then green with the magnitudes computed under np.errstate(over="ignore"). pandas' FloatArrayFormatter makes the same np.abs call unguarded (2.2 and 3.0), so this departs from it only in not warning. --- graphblas/core/formatting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphblas/core/formatting.py b/graphblas/core/formatting.py index f36ae04ea..50ae969cc 100644 --- a/graphblas/core/formatting.py +++ b/graphblas/core/formatting.py @@ -550,7 +550,11 @@ def format_with(fmt_str): result = format_with(f"{{value: .{digits:d}f}}") too_long = bool(result) and max(len(x) for x in result) > digits + 6 - abs_vals = np.abs(arr) + # A complex value with components near the float max has magnitude inf, + # which is what the checks below want. numpy 1.24 on arm64 also raises the + # FP overflow flag computing it, and that warning would escape from repr. + with np.errstate(over="ignore"): + abs_vals = np.abs(arr) has_large = bool((abs_vals > 1e6).any()) has_small = bool(((abs_vals < 10.0 ** (-digits)) & (abs_vals > 0)).any()) if has_small or (too_long and has_large): From 914b765b140dab4c705db828b0e221ad4c28c993 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 7 Nov 2026 20:32:32 -0600 Subject: [PATCH 4/5] Test the complex repr's chop_threshold on FC32 too test_chop_threshold_complex covered FC64 only. Run it on FC32 as well: its largest value renders, and a value below the threshold chops to complex zero, as for FC64. Replacing the chop's own type of zero with a float zero makes the FC32 case fail with the same IndexError. FC32 reaches the threshold by a different route. The COO frame hands the formatter python complexes, so abs() is computed in double and is finite where pandas computes an inf magnitude in float32. The two differ only for a threshold above the float32 max, so a comment notes it rather than code. --- graphblas/core/formatting.py | 1 + graphblas/tests/test_formatting.py | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/graphblas/core/formatting.py b/graphblas/core/formatting.py index 50ae969cc..581b9ff07 100644 --- a/graphblas/core/formatting.py +++ b/graphblas/core/formatting.py @@ -540,6 +540,7 @@ def _format_float_column(values, digits): # pandas does; e.g. a "%f"-style callable then casts complex the same way. return [na_rep if (v != v) else float_format(v) for v in arr] seq = arr.tolist() + # FC32 values come in as python complex, so a threshold > 3.4e38 can chop what pandas keeps. threshold = _display_option("chop_threshold") def format_with(fmt_str): diff --git a/graphblas/tests/test_formatting.py b/graphblas/tests/test_formatting.py index a141906ce..a85d1e590 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -6764,27 +6764,34 @@ def test_vector_as_matrix(): @pytest.mark.skipif("not pd") @pytest.mark.skipif("not dtypes._supports_complex") -def test_chop_threshold_complex(): +@pytest.mark.parametrize( + ("dtype", "big_str", "small_str"), + [ + ("FC32", "3.402823e+38+3.402823e+38j", "1.000000e-07+0.000000e+00j"), + ("FC64", "1.797693e+308+1.797693e+308j", "1.000000e-07+ 0.000000e+00j"), + ], +) +def test_chop_threshold_complex(dtype, big_str, small_str): # The COO (long) form formats complex columns through the float column # formatter, where display.chop_threshold compares each value's magnitude. - big = float(np.finfo(np.float64).max) + big = float(np.finfo(dtypes.lookup_dtype(dtype).np_type).max) v = Vector.from_coo( - [10_000, 500_000], [complex(big, big), complex(1e-7, 0)], size=10**6, dtype="FC64" + [10_000, 500_000], [complex(big, big), complex(1e-7, 0)], size=10**6, dtype=dtype ) with pd.option_context("display.chop_threshold", 1e-8): - # abs() of a python complex overflows for components near the float - # max; numpy's abs is inf, so nothing chops and this must render. + # abs() of a python complex overflows for FC64 components near the + # float max; numpy's abs is inf, so nothing chops and this must render. # Byte-identical to the pandas renderer, verified against 6f1eb02. r = repr(v) - assert "1.797693e+308+1.797693e+308j" in r - assert "1.000000e-07+ 0.000000e+00j" in r + assert big_str in r + assert small_str in r v._repr_html_() # A value below the threshold chops to complex zero. pandas 3.0.3 # itself crashes on this input (its own _trim_zeros_complex cannot # parse the float zero its chop substitutes into a complex column), # so rendering at all is deliberately better than byte-identity. w = Vector.from_coo( - [10_000, 500_000], [complex(1.0, 1.0), complex(5e-9, 0)], size=10**6, dtype="FC64" + [10_000, 500_000], [complex(1.0, 1.0), complex(5e-9, 0)], size=10**6, dtype=dtype ) r = repr(w) assert "1.000000e+00+1.000000e+00j" in r From 1ac58bfa70c50e3e8e0c9683d6a1a8e9575072d2 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 7 Nov 2026 20:34:19 -0600 Subject: [PATCH 5/5] Test that the complex repr's overflow warning cannot escape on any numpy Computing the magnitudes under np.errstate(over="ignore") fixed a RuntimeWarning that only numpy 1.24 on arm64 raises, so on any other numpy no test failed with that fix removed. Stand in for that numpy with an abs whose own arithmetic overflows (the squares of components near the float max), and check that repr and the HTML repr raise no RuntimeWarning for an FC64 value there. Without the errstate the test fails with "overflow encountered in multiply". FC32 values reach the formatter widened to double, so their magnitudes never overflow and the test covers FC64 only. --- graphblas/tests/test_formatting.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/graphblas/tests/test_formatting.py b/graphblas/tests/test_formatting.py index a85d1e590..8bb8877dc 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -1,4 +1,5 @@ import contextlib +import warnings import numpy as np import pytest @@ -6797,3 +6798,28 @@ def test_chop_threshold_complex(dtype, big_str, small_str): assert "1.000000e+00+1.000000e+00j" in r assert "0.000000e+00+0.000000e+00j" in r w._repr_html_() + + +@pytest.mark.skipif("not dtypes._supports_complex") +def test_complex_repr_magnitude_overflow_warns_nothing(monkeypatch): + # The float formatter takes np.abs of a complex column, which is inf for FC64 + # components near the float max. numpy 1.24 on arm64 also raises the FP + # overflow flag there (newer numpy does not), and that RuntimeWarning must not + # escape from repr. Stand in for that numpy with an abs whose own arithmetic + # overflows, so the test can fail on any numpy. (FC32 values reach the formatter + # widened to double, so their magnitudes never overflow.) + real_abs = np.abs + + def overflowing_abs(x): + x = np.asarray(x) + if np.iscomplexobj(x): + return np.sqrt(x.real * x.real + x.imag * x.imag) + return real_abs(x) + + big = float(np.finfo(np.float64).max) + v = Vector.from_coo([10_000, 500_000], [complex(big, big), 1j], size=10**6, dtype="FC64") + monkeypatch.setattr(formatting.np, "abs", overflowing_abs) + with warnings.catch_warnings(): + warnings.simplefilter("error", RuntimeWarning) + assert "1.797693e+308+1.797693e+308j" in repr(v) + v._repr_html_()