matplotlib__matplotlib-23476
DPI doubles after unpickling on M1 Mac — trace funnels localization into pickle/unpickle magic methods, enabling reranking to prioritize the correct serialization boundary.
Outcome
✅ Hit @1 (GT function) — lib/matplotlib/figure.py:Figure.__getstate__
Core idea: the selected test does not assert DPI directly, but its execution trace exposes a must-pass serialization skeleton
(__getstate__ / __setstate__), enabling accurate function-level reranking.
0) Metadata (Repro Anchors)
- Dataset: SWE-bench-Lite
- Instance ID:
matplotlib__matplotlib-23476 - Repository:
matplotlib/matplotlib - Base commit:
33a0599711d26dc2b79f851c6daed4947df7c167 - Issue (text reference):
https://github.com/matplotlib/matplotlib/issues/23471 - Ground Truth:
- ✅ GT edit location:
lib/matplotlib/figure.py:Figure.__getstate__ - Neighbor:
lib/matplotlib/figure.py:Figure.__setstate__ - ✅ GT test:
lib/matplotlib/tests/test_pickle.py::test_unpickle_canvas
- ✅ GT edit location:
1) Input
Symptom. On M1 MacBooks, repeatedly pickling/unpickling a Matplotlib figure causes its DPI to
double each time (200 → 400 → 800 → …), eventually triggering an OverflowError.
Issue excerpt (original, abridged)
The issue reports: “When a figure is unpickled, its dpi is doubled… if done in a loop it can cause an OverflowError.”
The reproduction script repeatedly pickle.dump/pickle.load a figure and prints the DPI, showing exponential growth.
The crash stack points to figure unpickling and backend manager creation on MacOSX.
[Bug]: DPI of a figure is doubled after unpickling on M1 Mac
...
0: 200.0
1: 400.0
2: 800.0
...
OverflowError: signed integer is greater than maximum
2) Step 1 — Relevant Test Retrieval (Two-Phase)
IssueExec localizes via issue → tests → traces → code.
BM25 prioritizes recall; the LLM then selects a small, high-quality set Td.
BM25 Top-10 (lexical filtering)
lib/matplotlib/tests/test_axes.py::test_vert_violinplot_custompoints_200
lib/matplotlib/tests/test_axes.py::test_horiz_violinplot_custompoints_200
lib/matplotlib/tests/test_arrow_patches.py::test_fancyarrow_dpi_cor_200dpi
lib/matplotlib/tests/test_figure.py::test_change_dpi
lib/matplotlib/tests/test_figure.py::test_subfigure_dpi
lib/matplotlib/tests/test_figure.py::test_set_fig_size
lib/matplotlib/tests/test_pickle.py::test_unpickle_canvas
lib/matplotlib/tests/test_figure.py::TestSubplotMosaic::test_user_order
lib/matplotlib/tests/test_figure.py::TestSubplotMosaic::test_nested_user_order
lib/matplotlib/tests/test_png.py::test_truncated_file
Observation: BM25 retrieves many “dpi/figure” tests; the serialization-specific signal becomes decisive in semantic selection.
LLM-selected Td (semantic selection)
lib/matplotlib/tests/test_pickle.py::test_unpickle_canvas
lib/matplotlib/tests/test_figure.py::test_change_dpi
lib/matplotlib/tests/test_figure.py::test_subfigure_dpi
lib/matplotlib/tests/test_pickle.py::test_simple
lib/matplotlib/tests/test_pickle.py::test_complete
Why this intermediate layer matters: the issue mentions DPI, but the mechanism is tied to pickle/unpickle state transitions. Test retrieval anchors the search to executable serialization behavior, preventing drift toward speculative backend-only hypotheses.
3) Step 2 — Trace-Guided Analysis (Execution Causal Skeleton)
Key retrieved test (GT test).
def test_unpickle_canvas():
fig = mfigure.Figure()
assert fig.canvas is not None
out = BytesIO()
pickle.dump(fig, out)
out.seek(0)
fig2 = pickle.load(out)
assert fig2.canvas is not None
Blind spot: this test checks canvas restoration, not DPI directly.
Value: the trace still exposes must-pass pickle/unpickle boundaries (__getstate__/__setstate__).
Coverage / call graph (test → callees)
lib/matplotlib/tests/test_pickle.py::test_unpickle_canvas →
• lib/matplotlib/_api/deprecation.py::wrapper
• lib/matplotlib/artist.py::Rectangle.__getstate__
• lib/matplotlib/cbook/__init__.py::CallbackRegistry.__getstate__
• lib/matplotlib/cbook/__init__.py::CallbackRegistry.__setstate__
• lib/matplotlib/figure.py::Figure.__getstate__
• lib/matplotlib/figure.py::Figure.__setstate__
• lib/matplotlib/transforms.py::Affine2D.__getstate__
• lib/matplotlib/transforms.py::Affine2D.__setstate__
• lib/matplotlib/transforms.py::Bbox.__getstate__
• lib/matplotlib/transforms.py::Bbox.__setstate__
• lib/matplotlib/transforms.py::BboxTransformTo.__getstate__
• lib/matplotlib/transforms.py::BboxTransformTo.__setstate__
• lib/matplotlib/transforms.py::TransformedBbox.__getstate__
• lib/matplotlib/transforms.py::TransformedBbox.__setstate__
Suspicious locations (trace-guided).
High/Critical candidates include Figure.__getstate__ and Figure.__setstate__, which are guaranteed to be on the unpickle path.
4) Step 3 — Contextual Refinement
IssueExec retrieves module structure and surrounding code context for files containing suspicious locations
(here, primarily lib/matplotlib/figure.py). This enables reranking to distinguish
root cause boundaries (state capture) from symptom-adjacent neighbors (state restore and backend interactions).
5) Step 4 — Final Reranking (Ranked Edit Locations)
Candidate contexts (abridged)
Location 1 (GT): Figure.__getstate__
def __getstate__(self):
state = super().__getstate__()
# The canvas cannot currently be pickled...
state.pop("canvas")
# Set cached renderer to None -- it can't be pickled.
state["_cachedRenderer"] = None
state['__mpl_version__'] = mpl.__version__
from matplotlib import _pylab_helpers
if self.canvas.manager in _pylab_helpers.Gcf.figs.values():
state['_restore_to_pylab'] = True
return state
Location 2 (neighbor): Figure.__setstate__
def __setstate__(self, state):
version = state.pop('__mpl_version__')
restore_to_pylab = state.pop('_restore_to_pylab', False)
if version != mpl.__version__:
_api.warn_external(...)
self.__dict__ = state
FigureCanvasBase(self) # Set self.canvas.
if restore_to_pylab:
import matplotlib.pyplot as plt
import matplotlib._pylab_helpers as pylab_helpers
...
plt.draw_if_interactive()
self.stale = True
Final ranked edit locations L* (Top-k).
- ✅
lib/matplotlib/figure.py:Figure.__getstate__(GT) lib/matplotlib/figure.py:Figure.__setstate__
Rationale: although the stacktrace points to __setstate__, repeated DPI doubling suggests a
state capture / serialization mismatch that compounds each pickle cycle.
Reranking therefore prioritizes the serialization boundary __getstate__.
6) Baseline (SWE-agent) — Failure Summary
- Reproduction failed: first writes
reproduce_bug.py, but the bug cannot be reproduced after execution (dpidoes not double as described in the issue), likely due to environment mismatch since the issue is reported on a Mac laptop. - Backend-first detour: then inspects
backend_macosx.py, but finds no code directly manipulatingdpi. - Repeated unsuccessful attempts: rewrites the reproduction script, still fails; then directly checks
rcsetup.pyfor DPI-related parameter definitions, but again finds nothing useful. - No successful fix: after repeated failed attempts, it deletes resize-related lines in
backend_macosx.py, but the issue is still not resolved.
In short, SWE-agent never establishes the issue → test → code link, so it drifts between failed reproduction and speculative file inspection without reaching the serialization root cause.
7) Step 5 — Patch Generation
To connect localization improvements to end-to-end repair (RQ2), we feed IssueExec’s function-level edit locations into the downstream Agentless repair module. Rather than providing entire files, the repair stage operates on a pinpointed function-level target with minimal surrounding context required for editing.
Why this fixes “DPI doubles after unpickling”: on high-DPI backends, the figure’s runtime DPI may be temporarily
scaled by device pixel ratio while the logical/original DPI is preserved in _original_dpi. If the scaled DPI
is serialized, repeated pickle/unpickle will compound the scaling. The patch therefore corrects the serialization boundary
in __getstate__ by restoring the logical DPI in the pickled state and removing _original_dpi.
Resolved patch
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -3023,6 +3023,15 @@ def __getstate__(self):
# Set cached renderer to None -- it can't be pickled.
state["_cachedRenderer"] = None
+ # High-DPI backends may temporarily scale the figure dpi by the device
+ # pixel ratio, keeping the logical/original dpi in _original_dpi.
+ # Do not pickle the scaled dpi (otherwise it will be scaled again on
+ # unpickle). Also drop _original_dpi from the pickled state.
+ _orig_dpi = state.pop("_original_dpi", None)
+ if _orig_dpi is not None:
+ state["_dpi"] = _orig_dpi
+
# add version information to the state
state['__mpl_version__'] = mpl.__version__
Outcome: this patch resolves the instance under the SWE-bench Lite evaluation setup. This supports our RQ2 claim: by pinpointing exact edit locations rather than providing entire files, the repair stage avoids being overwhelmed by extraneous code and focuses on the root-cause boundary.
8) Takeaway
This is a “trace-funneling” win: even when the retrieved test does not assert DPI, its execution trace exposes the correct low-entropy search space (serialization magic methods), enabling accurate function-level localization.