Skip to content

Stop RuntimeError FigureCanvasQTAgg has been deleted#152

Merged
jeanconn merged 3 commits intomasterfrom
hope
Dec 17, 2025
Merged

Stop RuntimeError FigureCanvasQTAgg has been deleted#152
jeanconn merged 3 commits intomasterfrom
hope

Conversation

@jeanconn
Copy link
Copy Markdown
Contributor

@jeanconn jeanconn commented Dec 10, 2025

Description

More fixes for xija_gui_fit to stop RuntimeError FigureCanvasQTAgg has been deleted.

This was required for me for xija_gui_fit to run at all when installed in ska3/hope on OSX - and seems to improve reliability for ska3/latest installs.

Without these fixes I was just getting this when I tried to run xija_gui_fit from the installed environment.

Adding plot  aacccdpt data__time
Adding plot  aacccdpt resid__time
Traceback (most recent call last):
  File "/Users/jean/miniforge3/envs/ska3-flight-2026.0rc4/lib/python3.13/site-packages/matplotlib/backend_bases.py", line 2926, in _wait_cursor_for_draw_cm
    yield
  File "/Users/jean/miniforge3/envs/ska3-flight-2026.0rc4/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py", line 385, in draw
    super().draw()
    ~~~~~~~~~~~~^^
  File "/Users/jean/miniforge3/envs/ska3-flight-2026.0rc4/lib/python3.13/site-packages/matplotlib/backends/backend_qt.py", line 492, in draw
    self.update()
    ~~~~~~~~~~~^^
RuntimeError: wrapped C/C++ object of type FigureCanvasQTAgg has been deleted

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/jean/miniforge3/envs/ska3-flight-2026.0rc4/lib/python3.13/site-packages/matplotlib/backends/backend_qt.py", line 523, in _draw_idle
    self.draw()
    ~~~~~~~~~^^
  File "/Users/jean/miniforge3/envs/ska3-flight-2026.0rc4/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py", line 380, in draw
    with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
          else nullcontext()):
          ^^^^^^^^^^^^^^^^^^
  File "/Users/jean/miniforge3/envs/ska3-flight-2026.0rc4/lib/python3.13/contextlib.py", line 162, in __exit__
    self.gen.throw(value)
    ~~~~~~~~~~~~~~^^^^^^^
  File "/Users/jean/miniforge3/envs/ska3-flight-2026.0rc4/lib/python3.13/site-packages/matplotlib/backend_bases.py", line 2928, in _wait_cursor_for_draw_cm
    self.canvas.set_cursor(self._last_cursor)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  File "/Users/jean/miniforge3/envs/ska3-flight-2026.0rc4/lib/python3.13/site-packages/matplotlib/backends/backend_qt.py", line 284, in set_cursor
    self.setCursor(_api.check_getitem(cursord, cursor=cursor))
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: wrapped C/C++ object of type FigureCanvasQTAgg has been deleted

Strangely, it worked fine running straight from the repo with python -m xija.gui_fit.app but didn't work after being installed. So that was just weird. But with these Qt changes the code changed to reliably running from either the repo or "as installed" with xija_gui_fit for me on OSX with a recent hope candidate.

This issue seems to be a recurrence of #126 which we believed fixed with #127 .

I don't entirely understand the timing and Qt.

Fixes #126 again

Interface impacts

Testing

Unit tests

  • Mac with a latest ska3
(latest) flame:xija jean$ pytest
=========================================================== test session starts ============================================================
platform darwin -- Python 3.12.8, pytest-8.3.4, pluggy-1.5.0
rootdir: /Users/jean/git
configfile: pytest.ini
plugins: anyio-4.7.0, timeout-2.3.1
collected 45 items                                                                                                                         

xija/tests/test_get_model_spec.py .......                                                                                            [ 15%]
xija/tests/test_models.py ......................................                                                                     [100%]

============================================================= warnings summary =============================================================
xija/xija/tests/test_models.py::test_dpa_real[True]
  /Users/jean/miniforge3/envs/latest/lib/python3.12/site-packages/setuptools_scm/git.py:312: UserWarning: git archive did not support describe output
    warnings.warn("git archive did not support describe output")

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
====================================================== 45 passed, 1 warning in 36.15

Independent check of unit tests by Javier

  • OSX + ska3-hope
(ska3-flight-2026.0rc9) ~/miniforge3/envs/ska3-flight-2026.0rc9/lib/python3.13/site-packages $ python -c "import xija; print(xija.__version__)"
4.35.1.dev5+g791f0eef8
(ska3-flight-2026.0rc9) ~/miniforge3/envs/ska3-flight-2026.0rc9/lib/python3.13/site-packages $ pytest xija/tests/test_get_model_spec.py::test_check_github_version
============================================================= test session starts =============================================================
platform darwin -- Python 3.13.10, pytest-9.0.1, pluggy-1.6.0
rootdir: /Users/javierg/miniforge3/envs/ska3-flight-2026.0rc9/lib/python3.13/site-packages
plugins: anyio-4.12.0, timeout-2.4.0
collected 1 item                                                                                                                              

xija/tests/test_get_model_spec.py .                                                                                                     [100%]

============================================================== 1 passed in 1.10s ==============================================================

Functional tests

For functional testing, I've made test conda packages and tested this on:

  • OSX with ska3 "latest"
  • OSX with ska3 "hope"
  • kady Linux with ska3 "hope" at rc8
  • kady Linux ska3 "latest"

My test was just to run "xija_gui_fit aca", add at least one plot, and run a fit. On each platform, I got a crash without this PR before xija_gui_fit really started and it worked OK as far as I could tell with the PR. On kady linux I note that my matplotlibrc is set to use TkAgg, so if I wanted to run xija_gui_fit without " newbackend, required_framework, current_framework))
ImportError: Cannot load backend 'TkAgg' which requires the 'tk' interactive framework, as 'qt' is currently running" I just specified the qt backend by env var with "env MPLBACKEND=qt5agg xija_gui_fit aca".

John Zuhone also noted additional functional testing in the comments.

Comment thread xija/gui_fit/plots.py
@jzuhone
Copy link
Copy Markdown
Collaborator

jzuhone commented Dec 10, 2025

@jeanconn the error you reported is one that's bedeviled me for years. I've spent way too many hours googling, conversing with ChatGPT, etc., trying to fix it.

How did you arrive at this particular solution?

@jeanconn
Copy link
Copy Markdown
Contributor Author

jeanconn commented Dec 10, 2025

Hi @jzuhone this fix largely came out of from fighting with Claude which convinced me that

By making PlotBox a QWidget, it can properly participate in Qt's parent-child hierarchy, preventing premature deletion of the underlying C++ FigureCanvas objects.

And it convinced me that the most critical fix is probably adding that extra singleShot to do a deferred update.

But let me know if you think it actually helps. I'm still trying to figure out if by solving one problem it has created others.

Comment thread xija/gui_fit/plots.py
@jeanconn
Copy link
Copy Markdown
Contributor Author

Also still trying to figure out if this can be trimmed down into a more isolated patch.

@jeanconn
Copy link
Copy Markdown
Contributor Author

It looks like the minimal change of

+++ b/xija/gui_fit/plots.py
@@ -870,9 +870,10 @@ class PlotsBox(QtWidgets.QVBoxLayout):
         print("Adding plot ", plot_name)
         plot_box = PlotBox(plot_name, self)
         self.addLayout(plot_box)
-        plot_box.update(first=True)
         self.plot_boxes.append(plot_box)
         self.plot_names.append(plot_name)
+        QtCore.QTimer.singleShot(0, lambda: plot_box.update(first=True))
+

gets this to work when installed in my ska3-hope environment, so maybe the extra parent/child handling to avoid premature garbage collection isn't really needed anyway.

@jeanconn
Copy link
Copy Markdown
Contributor Author

I also don't know if something about ska3-hope (with matplotlib 3.10.8 and qt 5.15.15) has made the "RuntimeError: wrapped C/C++ object of type FigureCanvasQTAgg has been deleted" more reliable?

@jzuhone
Copy link
Copy Markdown
Collaborator

jzuhone commented Dec 16, 2025

@jeanconn I tried these changes on my current Ska installation on my laptop, with:

python 3.12.8
matplotlib 3.10.0
qt 5.15.8

I used it on the CEA model, which has reliably crashed on me while attempting various plot modifications. It didn't crash this time, and everything else seemed to work, so I think this patch is great.

@jzuhone
Copy link
Copy Markdown
Collaborator

jzuhone commented Dec 16, 2025

@jeanconn I don't think the single-shot is enough, because I've tried that before. This bug is pretty intermittent. I would support your whole patch here.

@jeanconn
Copy link
Copy Markdown
Contributor Author

Oh awesome. Thanks for the functional testing @jzuhone! If it looks like there's some value here we'll get this out of draft status and move this along.

@jzuhone jzuhone marked this pull request as ready for review December 17, 2025 00:04
@jeanconn jeanconn changed the title Modifications for xija_gui_fit installed in ska3-hope More fixes for xija_gui_fit to stop RuntimeError FigureCanvasQTAgg has been deleted Dec 17, 2025
@jeanconn jeanconn changed the title More fixes for xija_gui_fit to stop RuntimeError FigureCanvasQTAgg has been deleted Stop RuntimeError FigureCanvasQTAgg has been deleted Dec 17, 2025
Copy link
Copy Markdown
Contributor

@javierggt javierggt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just:

  • ran the tests
  • verified that master crashes with the error in the description, and the branch succeeds
  • went over the code changes

The changes seem reasonable to me. I would point out that having something inherit from a layout class and then have other data members is very unusual, and probably should not be done. There is still another class that inherits from QVBoxLayout (PlotsBox) but we are not refactoring the whole thing now. I understand.

This seems like a matplotlib issue to me.

There are a few changes that smell like this type of error. But I think the source of the problem could be the lines in FitStatWindow:

        self.fig = Figure()
        canvas = FigureCanvas(self.fig)
        self.canvas = canvas

where the local variable canvas goes out of scope and is garbage collected. This should not happen, because the parent of the canvas is correctly passed in the constructor, so the parent QWidget should keep a reference to it until itself is destroyed

I looked into the source of FigureCanvasQT (its a base of FigureCanvas), and these are the relevant lines:

class FigureCanvasQT(FigureCanvasBase, QtWidgets.QWidget):
    def __init__(self, figure=None):
        super().__init__(figure=figure)

See? super().__init__(figure=figure) only calls the __init__ method from FigureCanvasBase, not QWidget. That is the cause of the issue.

As an example, run the following

class A:
    def __init__(self, *args, **kwargs):
        print(f"A args={args} kwargs={kwargs}")
class B:
    def __init__(self, *args, **kwargs):
        print(f"B args={args} kwargs={kwargs}")
class C(A, B):
    def __init__(self, figure):
        super().__init__(figure=figure)
c = C(figure=None)

Another possible cause for issues like this (but not this one)

Another change in the same spirit, but I don't think it caused the original issue is this line in the constructor of app.MainLeftPanel:

container = QtWidgets.QWidget()

which creates a widget that is then collected when the constructor exits. I expect that the owner of the widget changes in this line:

self.scroll.setWidget(container)

at least in the C++ side it does that. Who knows if the python stuff is properly implemented.

@jeanconn
Copy link
Copy Markdown
Contributor Author

Awesome. Thanks for the deep dive @javierggt . Do you think we should open a new issue and put your research in there if we decide to implement more fixes or suggest upstream fixes? Or just try to remember to come back to this PR if we want to do more?

@javierggt
Copy link
Copy Markdown
Contributor

In summary, I believe the actual fix was making sure the canvas was a data member of the FitStatWindow class, which prevented the canvas from being garbage collected. Once it is a data member, self.canvas is freed at the same time as self.fig.

@javierggt
Copy link
Copy Markdown
Contributor

I was thinking we could report this upstream. I don't have a strong opinion on where to keep it here.

@jeanconn jeanconn merged commit eec2dba into master Dec 17, 2025
2 checks passed
@jeanconn jeanconn deleted the hope branch December 17, 2025 22:32
This was referenced Jan 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RuntimeError trying to run xija_gui_fit on Mac

3 participants