diff --git a/autofit/mapper/prior_model/abstract.py b/autofit/mapper/prior_model/abstract.py index 6dee7ea7b..0f3e16dc9 100644 --- a/autofit/mapper/prior_model/abstract.py +++ b/autofit/mapper/prior_model/abstract.py @@ -1,4 +1,5 @@ import copy +import functools import inspect import json import logging @@ -1859,7 +1860,7 @@ def order_no(self) -> str: ] return ":".join(values) - @property + @functools.cached_property def parameterization(self) -> str: """ Describes the path to each of the PriorModels, its class diff --git a/autofit/non_linear/live_viewer.py b/autofit/non_linear/live_viewer.py index f3361bc84..5b63b67aa 100644 --- a/autofit/non_linear/live_viewer.py +++ b/autofit/non_linear/live_viewer.py @@ -40,12 +40,33 @@ HEADLESS_BACKENDS = {"agg", "pdf", "ps", "svg", "cairo", "template"} -def _backend_can_show() -> bool: - """Return True if the active matplotlib backend can display a window.""" +_INTERACTIVE_BACKENDS = ("TkAgg", "QtAgg", "Qt5Agg", "GTK3Agg", "GTK4Agg", "WXAgg", "macosx") + + +def _ensure_interactive_backend() -> bool: + """Try to switch to an interactive matplotlib backend. + + The viewer subprocess inherits the parent process's backend, which is + typically ``Agg`` (the search process uses it for headless PNG + rendering). Since this subprocess exists specifically to show a + window, we try each interactive backend until one sticks. Returns + ``True`` if the active backend can display a window after the + attempt. + """ import matplotlib backend = matplotlib.get_backend().lower() - return not any(backend.startswith(name) for name in HEADLESS_BACKENDS) + if not any(backend.startswith(name) for name in HEADLESS_BACKENDS): + return True + + for candidate in _INTERACTIVE_BACKENDS: + try: + matplotlib.use(candidate) + return True + except ImportError: + continue + + return False def _install_signal_handlers(stop_event): @@ -57,12 +78,14 @@ def _handle(signum, frame): def run(image_path: Path, title: str) -> int: - import matplotlib + if not _ensure_interactive_backend(): + import matplotlib - if not _backend_can_show(): logger.warning( - "live_viewer: matplotlib backend %r cannot display a window; " - "live visualization disabled (PNG writes to %s continue normally).", + "live_viewer: no interactive matplotlib backend found (tried %s, " + "active backend is %r). Install python3-tk or another GUI toolkit " + "to enable live visualization. PNG writes to %s continue normally.", + ", ".join(_INTERACTIVE_BACKENDS), matplotlib.get_backend(), image_path, )