From 4564ae9a1183ec93055f5017bbef97973a03fcff Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 27 May 2026 14:35:37 +0100 Subject: [PATCH] Cache model.parameterization; try interactive matplotlib backends Two quick-update performance fixes: 1. Cache `AbstractPriorModel.parameterization` via @functools.cached_property. For MGE models with 40 Gaussians the recursive tree walk triggers 14M function calls (~2.7s). Since the model structure is immutable during a search, caching reduces subsequent calls from 2.7s to 0.05s. 2. live_viewer.py: try interactive matplotlib backends (TkAgg, QtAgg, etc.) before falling back to the inherited Agg backend. The viewer subprocess inherits the parent's Agg (used for headless PNG rendering) but needs a GUI backend to display a window. Co-Authored-By: Claude Opus 4.7 --- autofit/mapper/prior_model/abstract.py | 3 ++- autofit/non_linear/live_viewer.py | 37 +++++++++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) 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, )