From eb5878ed5bebc96710200848fd24356942bae3fc Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 5 Aug 2026 01:50:53 -0500 Subject: [PATCH 1/2] 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 cd9380c0c..63c06f747 100644 --- a/graphblas/core/formatting.py +++ b/graphblas/core/formatting.py @@ -443,7 +443,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 264dc0502..3659827a5 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -4931,3 +4931,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 d09186f698d891af692f9ece1e9909e120e0c129 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 6 Aug 2026 00:37:10 -0700 Subject: [PATCH 2/2] 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 3659827a5..8f41dcb7d 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -4934,6 +4934,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.