From 934c7680d82b6d757b7f0af85e5152c933ce15b9 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 21:22:58 +0000 Subject: [PATCH 01/41] Move the GUI from PyQt6 to PySide6 The gui extra and the dev group now install PySide6-Essentials, pyqtgraph and superqt; pytest-qt runs on PySide6. The code changes are the binding's names (Signal, shiboken6 for object validity) with no change of behaviour. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- README.md | 6 +- changelog.d/gui-pyside6.changed.md | 1 + croak/gui/__init__.py | 4 +- croak/gui/base.py | 8 +-- croak/gui/branding.py | 8 +-- croak/gui/canvas.py | 6 +- croak/gui/help.py | 2 +- croak/gui/mirror_stack.py | 18 +++--- croak/gui/panel_pages.py | 4 +- croak/gui/readout.py | 8 +-- croak/gui/settings.py | 14 +++-- croak/gui/stage_dispersion.py | 2 +- croak/gui/stage_load.py | 2 +- croak/gui/stage_marginal_check.py | 4 +- croak/gui/stage_preprocess.py | 8 +-- croak/gui/stage_retrieve.py | 4 +- croak/gui/stage_simulated.py | 4 +- croak/gui/stage_synthetic.py | 2 +- croak/gui/stage_uncertainty.py | 6 +- croak/gui/stage_welcome.py | 4 +- croak/gui/state.py | 16 +++--- croak/gui/widgets.py | 10 ++-- croak/gui/wizard.py | 4 +- croak/gui/worker.py | 22 +++---- docs/getting_started/installation.md | 4 +- docs/reference/features.md | 2 +- pyproject.toml | 8 ++- tests/conftest.py | 2 +- tests/gui/test_base.py | 2 +- tests/gui/test_branding.py | 2 +- tests/gui/test_canvas.py | 2 +- tests/gui/test_gui_package.py | 4 +- tests/gui/test_help.py | 4 +- tests/gui/test_mirror_stack.py | 2 +- tests/gui/test_panel_pages.py | 2 +- tests/gui/test_settings.py | 4 +- tests/gui/test_stage_dispersion.py | 2 +- tests/gui/test_stage_load.py | 4 +- tests/gui/test_stage_marginal_check.py | 2 +- tests/gui/test_stage_preprocess.py | 8 +-- tests/gui/test_stage_retrieve.py | 4 +- tests/gui/test_stage_simulated.py | 4 +- tests/gui/test_stage_synthetic.py | 2 +- tests/gui/test_stage_uncertainty.py | 10 ++-- tests/gui/test_state.py | 2 +- tests/gui/test_widgets.py | 4 +- tests/gui/test_wizard.py | 4 +- tests/gui/test_wizard_flow.py | 35 +++++------ tests/gui/test_worker.py | 2 +- tools/make_docs_images.py | 4 +- uv.lock | 80 +++++++++++++------------- 52 files changed, 192 insertions(+), 182 deletions(-) create mode 100644 changelog.d/gui-pyside6.changed.md diff --git a/AGENTS.md b/AGENTS.md index ed6fa29..4f202e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ rather than style, ask rather than guess. | Python | 3.14, equal to `requires-python` in `pyproject.toml` | | Type checker | `pyright`, `basic` mode, over `croak/` | | Numerics | NumPy/SciPy, with JAX (optimistix, lineax) for the differentiable solvers | -| GUI | the `gui` extra (PyQt6, superqt); the dev group always installs it, and its tests run offscreen with pytest-qt | +| GUI | the `gui` extra (PySide6, pyqtgraph, superqt); the dev group always installs it, and its tests run offscreen with pytest-qt | | Optional extras | `evo` (evosax, the `cma-es` retriever; in the dev group) | | Planned work | [GitHub issues](https://github.com/LupoLab/croak/issues). [`CONTRIBUTING.md`](CONTRIBUTING.md) says how to write an issue and how to label an issue or a pull request | diff --git a/README.md b/README.md index 5970f55..6759f14 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ the PNPS (parameterised nonlinear process spectra) class of measurements, currently focused on FROG: the SHG, SD and PG/transient-grating geometries, ten solvers over a single physical forward model, and the workflow around them (loading, cleaning, retrieval, post-processing, dispersion tuning, uncertainty, -plotting and saving). The library is headless (NumPy/SciPy/JAX); a PyQt6 GUI is +plotting and saving). The library is headless (NumPy/SciPy/JAX); a Qt GUI (PySide6) is optional. What distinguishes croak from a classic FROG code is the forward model. @@ -96,7 +96,7 @@ uv run croak-gui
Platform notes: read this if the install fails -The binary dependencies (`jax`, `nlopt`, `finufft`, `pyqt6`) do not publish +The binary dependencies (`jax`, `nlopt`, `finufft`, `pyside6-essentials`) do not publish wheels for every platform. Where a wheel is missing, pip/uv falls back to building from source, which needs CMake and a C++ toolchain. @@ -233,7 +233,7 @@ from the underlying propagation physics. ## GUI -A PyQt6 wizard (Load → Marginal check → Preprocess → Retrieve → Dispersion → +A PySide6 wizard (Load → Marginal check → Preprocess → Retrieve → Dispersion → Uncertainty) drives the same library functions, with embedded plots, threaded retrieval and a live preview: diff --git a/changelog.d/gui-pyside6.changed.md b/changelog.d/gui-pyside6.changed.md new file mode 100644 index 0000000..99908b5 --- /dev/null +++ b/changelog.d/gui-pyside6.changed.md @@ -0,0 +1 @@ +The GUI runs on PySide6 instead of PyQt6, and the `gui` extra now installs `pyside6-essentials`, `pyqtgraph` and `superqt`. Code that embeds croak's GUI widgets in its own Qt application must create a PySide6 `QApplication`. diff --git a/croak/gui/__init__.py b/croak/gui/__init__.py index 8efab18..ee7372d 100644 --- a/croak/gui/__init__.py +++ b/croak/gui/__init__.py @@ -1,4 +1,4 @@ -"""PyQt6 Qt-Widgets wizard GUI for croak (matplotlib plots). +"""PySide6 Qt-Widgets wizard GUI for croak (matplotlib plots). The GUI is a thin orchestration layer over the croak library: it loads, cleans, retrieves, post-processes, tunes dispersion, plots and saves — all via the @@ -37,7 +37,7 @@ def run_wizard(argv=None) -> int: through untouched, so Qt's own flags (``-style``, ``-platform``, …) still work. """ try: - from PyQt6.QtWidgets import QApplication + from PySide6.QtWidgets import QApplication except ImportError as exc: # the gui extra is optional — say how to get it raise ImportError( "croak's GUI needs the optional Qt dependencies, which are not " diff --git a/croak/gui/base.py b/croak/gui/base.py index 6ea58b8..715d212 100644 --- a/croak/gui/base.py +++ b/croak/gui/base.py @@ -2,9 +2,9 @@ from __future__ import annotations -from PyQt6.QtCore import Qt, QTimer -from PyQt6.QtGui import QShowEvent -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QShowEvent +from PySide6.QtWidgets import ( QHBoxLayout, QLabel, QScrollArea, @@ -29,7 +29,7 @@ class Stage(QWidget): """A wizard page: a scrollable control column on the left, plots on the right. - The two sit on a :class:`~PyQt6.QtWidgets.QSplitter`, so on a small display the + The two sit on a :class:`~PySide6.QtWidgets.QSplitter`, so on a small display the control column can be narrowed or dragged fully closed to give the plots the room; its width is shared by every stage and remembered between launches (:mod:`croak.gui.settings`). The plot side never collapses. diff --git a/croak/gui/branding.py b/croak/gui/branding.py index c674ca7..25bbcae 100644 --- a/croak/gui/branding.py +++ b/croak/gui/branding.py @@ -5,7 +5,7 @@ :func:`~croak.gui.run_wizard`. This module imports Qt, so it must only be imported from *inside* the functions -that already need Qt: importing :mod:`croak.gui` deliberately does not pull PyQt6 +that already need Qt: importing :mod:`croak.gui` deliberately does not pull PySide6 in, so the library stays usable headless. """ @@ -14,8 +14,8 @@ import sys from pathlib import Path -from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QApplication +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QApplication __all__ = ["APP_NAME", "ORGANISATION_NAME", "ICON_PATH", "app_icon", "apply_identity"] @@ -46,7 +46,7 @@ def app_icon() -> QIcon: Returns ------- - PyQt6.QtGui.QIcon + PySide6.QtGui.QIcon The icon, carrying a pixmap at each of :data:`_ICON_SIZES`. Null only if the SVG is missing or Qt's SVG image plugin is unavailable. """ diff --git a/croak/gui/canvas.py b/croak/gui/canvas.py index 1793dbd..455284f 100644 --- a/croak/gui/canvas.py +++ b/croak/gui/canvas.py @@ -9,9 +9,9 @@ from matplotlib.backends.backend_qt import NavigationToolbar2QT from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg from matplotlib.figure import Figure -from PyQt6.QtCore import QTimer -from PyQt6.QtGui import QResizeEvent -from PyQt6.QtWidgets import ( +from PySide6.QtCore import QTimer +from PySide6.QtGui import QResizeEvent +from PySide6.QtWidgets import ( QCheckBox, QComboBox, QDoubleSpinBox, diff --git a/croak/gui/help.py b/croak/gui/help.py index 69794a1..139f48f 100644 --- a/croak/gui/help.py +++ b/croak/gui/help.py @@ -18,7 +18,7 @@ from html import escape -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QCheckBox, QDialog, QDialogButtonBox, diff --git a/croak/gui/mirror_stack.py b/croak/gui/mirror_stack.py index 8657ea5..63727a9 100644 --- a/croak/gui/mirror_stack.py +++ b/croak/gui/mirror_stack.py @@ -17,8 +17,8 @@ import contextlib -from PyQt6.QtCore import pyqtSignal -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Signal +from PySide6.QtWidgets import ( QComboBox, QFileDialog, QFormLayout, @@ -57,10 +57,10 @@ class MirrorRow(QWidget): removal); chirped compressors and custom files are phase-only. """ - changed = pyqtSignal() - status = pyqtSignal(str) - remove_requested = pyqtSignal(object) - auto_requested = pyqtSignal(object) + changed = Signal() + status = Signal(str) + remove_requested = Signal(object) + auto_requested = Signal(object) def __init__(self, parent: QWidget | None = None): super().__init__(parent) @@ -319,9 +319,9 @@ def cleanup(self) -> None: class MirrorStack(QWidget): """An editable list of :class:`MirrorRow` widgets plus an 'Add' button.""" - changed = pyqtSignal() - status = pyqtSignal(str) - auto_requested = pyqtSignal(object) + changed = Signal() + status = Signal(str) + auto_requested = Signal(object) def __init__(self, parent: QWidget | None = None): super().__init__(parent) diff --git a/croak/gui/panel_pages.py b/croak/gui/panel_pages.py index a74abb9..16d6a57 100644 --- a/croak/gui/panel_pages.py +++ b/croak/gui/panel_pages.py @@ -24,8 +24,8 @@ from collections.abc import Mapping, Sequence from matplotlib.figure import Figure -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QDialog, QTabWidget, QToolButton, QVBoxLayout, QWidget +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QDialog, QTabWidget, QToolButton, QVBoxLayout, QWidget from .. import plotting from ..plotting import ( diff --git a/croak/gui/readout.py b/croak/gui/readout.py index e272617..d531013 100644 --- a/croak/gui/readout.py +++ b/croak/gui/readout.py @@ -30,9 +30,9 @@ from __future__ import annotations -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QFontDatabase -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt +from PySide6.QtGui import QFontDatabase +from PySide6.QtWidgets import ( QGridLayout, QGroupBox, QHBoxLayout, @@ -127,7 +127,7 @@ def _cell(*, bold: bool = False, fixed_width: bool = False) -> QLabel: class RetrievalReadout(QWidget): """The parameter table and scalar read-outs beneath the retrieval figure. - Built from plain :class:`~PyQt6.QtWidgets.QLabel` grids rather than a table + Built from plain :class:`~PySide6.QtWidgets.QLabel` grids rather than a table widget: the labels size themselves to their content, so the strip stays the few-line height the figure can spare without any per-style height arithmetic, and a refresh is a handful of ``setText`` calls with no item allocation. diff --git a/croak/gui/settings.py b/croak/gui/settings.py index a7e486c..520812c 100644 --- a/croak/gui/settings.py +++ b/croak/gui/settings.py @@ -3,7 +3,7 @@ The wizard remembers how its user last arranged it — the un-maximised window size and position, the width of the control column, which auxiliary strips were folded — so a small-screen user sets things up once. Values live in a -:class:`~PyQt6.QtCore.QSettings` store in INI format under the user scope, keyed +:class:`~PySide6.QtCore.QSettings` store in INI format under the user scope, keyed by the organisation and application names from :mod:`.branding`; passing those explicitly means the store is the same whether or not :func:`~croak.gui.branding.apply_identity` has run (the tests build a @@ -19,9 +19,11 @@ from __future__ import annotations -from PyQt6.QtCore import QByteArray, QSettings, QSize -from PyQt6.QtGui import QGuiApplication -from PyQt6.QtWidgets import QWidget +from typing import cast + +from PySide6.QtCore import QByteArray, QSettings, QSize +from PySide6.QtGui import QGuiApplication +from PySide6.QtWidgets import QWidget from .branding import APP_NAME, ORGANISATION_NAME @@ -128,7 +130,9 @@ def save_window(window: QWidget) -> None: def controls_width() -> int: """The remembered control-column width (logical px), else the default.""" - return int(user_settings().value(KEY_CONTROLS_WIDTH, DEFAULT_CONTROLS_WIDTH, int)) + # ``type=int`` makes Qt return an int; PySide6's stubs only say ``object``. + stored = user_settings().value(KEY_CONTROLS_WIDTH, DEFAULT_CONTROLS_WIDTH, int) + return int(cast(int, stored)) def set_controls_width(px: int) -> None: diff --git a/croak/gui/stage_dispersion.py b/croak/gui/stage_dispersion.py index 5b5610b..6ebc048 100644 --- a/croak/gui/stage_dispersion.py +++ b/croak/gui/stage_dispersion.py @@ -12,7 +12,7 @@ from dataclasses import replace import numpy as np -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QFileDialog, QHBoxLayout, QLabel, diff --git a/croak/gui/stage_load.py b/croak/gui/stage_load.py index b249dd8..d3117ad 100644 --- a/croak/gui/stage_load.py +++ b/croak/gui/stage_load.py @@ -13,7 +13,7 @@ import os import numpy as np -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QComboBox, QFileDialog, QHBoxLayout, diff --git a/croak/gui/stage_marginal_check.py b/croak/gui/stage_marginal_check.py index 2e42eb2..ba4ad2d 100644 --- a/croak/gui/stage_marginal_check.py +++ b/croak/gui/stage_marginal_check.py @@ -27,8 +27,8 @@ from __future__ import annotations import numpy as np -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QHBoxLayout, QPushButton, QSplitter, QWidget +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QHBoxLayout, QPushButton, QSplitter, QWidget from .. import io from ..marginal_checks import ( diff --git a/croak/gui/stage_preprocess.py b/croak/gui/stage_preprocess.py index ce0a26c..f43263b 100644 --- a/croak/gui/stage_preprocess.py +++ b/croak/gui/stage_preprocess.py @@ -24,9 +24,9 @@ from dataclasses import replace from typing import TypedDict -from PyQt6 import sip -from PyQt6.QtCore import QTimer -from PyQt6.QtWidgets import QHBoxLayout, QPushButton, QWidget +import shiboken6 +from PySide6.QtCore import QTimer +from PySide6.QtWidgets import QHBoxLayout, QPushButton, QWidget from .. import io, plotting from ..preprocess import suggested_tau_exclude @@ -646,7 +646,7 @@ def _discard_worker(self, worker): # The worker is parented to this stage, so when the wizard rebuilds its # stages (``deleteLater`` on each) between the thread finishing and this # queued slot running, Qt has already destroyed it with its parent. - if not sip.isdeleted(worker): + if shiboken6.isValid(worker): worker.deleteLater() def _show_tracedata(self, td): diff --git a/croak/gui/stage_retrieve.py b/croak/gui/stage_retrieve.py index 749a5ba..927ca51 100644 --- a/croak/gui/stage_retrieve.py +++ b/croak/gui/stage_retrieve.py @@ -11,8 +11,8 @@ import time import numpy as np -from PyQt6.QtCore import QTimer -from PyQt6.QtWidgets import ( +from PySide6.QtCore import QTimer +from PySide6.QtWidgets import ( QFileDialog, QHBoxLayout, QPushButton, diff --git a/croak/gui/stage_simulated.py b/croak/gui/stage_simulated.py index 1c2cc59..430ae71 100644 --- a/croak/gui/stage_simulated.py +++ b/croak/gui/stage_simulated.py @@ -17,8 +17,8 @@ from __future__ import annotations import numpy as np -from PyQt6.QtCore import QTimer -from PyQt6.QtWidgets import ( +from PySide6.QtCore import QTimer +from PySide6.QtWidgets import ( QComboBox, QFileDialog, QHBoxLayout, diff --git a/croak/gui/stage_synthetic.py b/croak/gui/stage_synthetic.py index 0745278..6405f3a 100644 --- a/croak/gui/stage_synthetic.py +++ b/croak/gui/stage_synthetic.py @@ -17,7 +17,7 @@ import math import numpy as np -from PyQt6.QtCore import QTimer +from PySide6.QtCore import QTimer from .. import dispersion from ..constants import C diff --git a/croak/gui/stage_uncertainty.py b/croak/gui/stage_uncertainty.py index 8f1138a..ed0be78 100644 --- a/croak/gui/stage_uncertainty.py +++ b/croak/gui/stage_uncertainty.py @@ -13,8 +13,8 @@ import os -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( QFileDialog, QHBoxLayout, QListWidget, @@ -319,7 +319,7 @@ def _run(self): if self.state.result is None or self.state.tracedata is None: self.set_status("Retrieve a pulse first.") return - # Import here so the rest of the GUI imports even if PyQt-less envs build + # Import here so the rest of the GUI imports even if Qt-less envs build # the worker lazily; the worker pulls in the (heavier) solver stack. from .worker import UncertaintyWorker diff --git a/croak/gui/stage_welcome.py b/croak/gui/stage_welcome.py index 0ebf867..4ffa0eb 100644 --- a/croak/gui/stage_welcome.py +++ b/croak/gui/stage_welcome.py @@ -9,8 +9,8 @@ from __future__ import annotations -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( QLabel, QPushButton, QSizePolicy, diff --git a/croak/gui/state.py b/croak/gui/state.py index 0145f3f..30eb121 100644 --- a/croak/gui/state.py +++ b/croak/gui/state.py @@ -12,7 +12,7 @@ from dataclasses import fields from typing import TYPE_CHECKING -from PyQt6.QtCore import QObject, pyqtSignal +from PySide6.QtCore import QObject, Signal from ..session.options import SessionOptions from ..session.params import ( @@ -47,16 +47,16 @@ class WizardState(QObject): """Holds wizard parameters and pipeline outputs, with change signals.""" - stage_changed = pyqtSignal(int) - load_data_changed = pyqtSignal() - tracedata_changed = pyqtSignal() - result_changed = pyqtSignal() - uncertainty_changed = pyqtSignal() - status = pyqtSignal(str) + stage_changed = Signal(int) + load_data_changed = Signal() + tracedata_changed = Signal() + result_changed = Signal() + uncertainty_changed = Signal() + status = Signal(str) # Request the wizard to point the current session at a new dataset folder # (its argument) and replay load → preprocess → retrieve. Emitted by the # Retrieve stage's "Retarget…" button; handled in croak.gui.wizard.Wizard. - retarget_requested = pyqtSignal(str) + retarget_requested = Signal(str) def __init__(self): super().__init__() diff --git a/croak/gui/widgets.py b/croak/gui/widgets.py index 14803bf..301d680 100644 --- a/croak/gui/widgets.py +++ b/croak/gui/widgets.py @@ -10,9 +10,9 @@ import math -from PyQt6.QtCore import Qt, pyqtSignal -from PyQt6.QtGui import QValidator -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QValidator +from PySide6.QtWidgets import ( QDoubleSpinBox, QHBoxLayout, QSizePolicy, @@ -106,7 +106,7 @@ class RangeControl(QWidget): box. Programmatic :meth:`set_bounds`/:meth:`set_values` are signal-safe. """ - changed = pyqtSignal(float, float) + changed = Signal(float, float) def __init__( self, @@ -201,7 +201,7 @@ class ValueControl(QWidget): ``changed(value)`` fires on slider drag or spin commit. """ - changed = pyqtSignal(float) + changed = Signal(float) def __init__( self, diff --git a/croak/gui/wizard.py b/croak/gui/wizard.py index ae04932..3aa0ab6 100644 --- a/croak/gui/wizard.py +++ b/croak/gui/wizard.py @@ -14,8 +14,8 @@ import os import numpy as np -from PyQt6.QtGui import QCloseEvent -from PyQt6.QtWidgets import ( +from PySide6.QtGui import QCloseEvent +from PySide6.QtWidgets import ( QFileDialog, QHBoxLayout, QLabel, diff --git a/croak/gui/worker.py b/croak/gui/worker.py index 8bef93d..12cf50f 100644 --- a/croak/gui/worker.py +++ b/croak/gui/worker.py @@ -10,7 +10,7 @@ import time import numpy as np -from PyQt6.QtCore import QThread, pyqtSignal +from PySide6.QtCore import QThread, Signal from ..session import pipeline @@ -47,8 +47,8 @@ class PreprocessWorker(QThread): An error message and the originating ``request_id``. """ - finished_ok = pyqtSignal(object, int, bool) - failed = pyqtSignal(str, int) + finished_ok = Signal(object, int, bool) + failed = Signal(str, int) def __init__(self, params, data, request_id, parent=None, *, fast=False): super().__init__(parent) @@ -110,11 +110,11 @@ class RetrievalWorker(QThread): #: Python-heavy solver. PREVIEW_DUTY = 0.25 - progress = pyqtSignal(int, float, float) - preview = pyqtSignal(object) - finished_ok = pyqtSignal(object, float) - stopped = pyqtSignal(object, float) - failed = pyqtSignal(str) + progress = Signal(int, float, float) + preview = Signal(object) + finished_ok = Signal(object, float) + stopped = Signal(object, float) + failed = Signal(str) def __init__( self, @@ -236,9 +236,9 @@ class UncertaintyWorker(QThread): An error message on failure (excluding a user Stop). """ - progress = pyqtSignal(int, int, float) - finished_ok = pyqtSignal(object) - failed = pyqtSignal(str) + progress = Signal(int, int, float) + finished_ok = Signal(object) + failed = Signal(str) def __init__( self, result, tracedata, retrieve_params, uparams, dispersion=None, parent=None diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index bdca53b..c146ba6 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -62,7 +62,7 @@ activate it, then `pip install -e .`. For work on croak itself, continue with th ## Supported platforms -croak's binary dependencies (`jax`, `nlopt`, `finufft` and, for the GUI, `pyqt6`) +croak's binary dependencies (`jax`, `nlopt`, `finufft` and, for the GUI, `pyside6-essentials`) publish wheels for a limited set of platforms. Where a wheel is missing, the installer falls back to building from source, which needs CMake and a C++ toolchain. @@ -141,7 +141,7 @@ list current driver and hardware requirements. | Delay filtering | `finufft` | | Data and sessions | `h5py`, `tomli-w` | | Plotting and progress | `matplotlib`, `tqdm` | -| GUI (`gui` extra) | `pyqt6`, `superqt` | +| GUI (`gui` extra) | `pyside6-essentials`, `pyqtgraph`, `superqt` | Importing `croak` does not import Qt. The library and session replay work without the graphical interface. diff --git a/docs/reference/features.md b/docs/reference/features.md index 29bc2c3..29300cf 100644 --- a/docs/reference/features.md +++ b/docs/reference/features.md @@ -234,7 +234,7 @@ Further reading: [Estimating FWHM uncertainty](../howto/uncertainty.md), ## Interfaces - **Library first** — every stage is callable without the GUI. -- **The wizard** — a six-stage PyQt6 application (Load → Marginal check → +- **The wizard** — a six-stage PySide6 application (Load → Marginal check → Preprocess → Retrieve → Dispersion → Uncertainty) with live retrieval previews and a numeric read-out of retrieved, measured, transform-limited and (for synthetic sessions) true pulse parameters. Laid out for 1366×768 displays and up. diff --git a/pyproject.toml b/pyproject.toml index 72bbdf9..2ec885a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,9 @@ dependencies = [ # scripted pipelines install without Qt: `pip install croak` (or # `uv sync --no-dev`). The GUI needs `pip install "croak[gui]"`; launching # `croak-gui` without it raises a message saying exactly that. -gui = ["pyqt6>=6.11.0", "superqt>=0.8.2"] +# PySide6-Essentials is the Qt modules the window uses (about a third of PySide6); +# pyqtgraph draws every on-screen plot, matplotlib only the exported figures. +gui = ["pyside6-essentials>=6.10.1", "pyqtgraph>=0.14", "superqt>=0.8.2"] # Global-search retriever (the 'cma-es' algorithm): CMA-ES / Sep-CMA-ES / DE. # JAX-native evolution strategies; reuses the existing jax install. evo = ["evosax>=0.2.0"] @@ -90,7 +92,8 @@ dev = [ # The GUI extra's packages, spelled out: the dev environment always has Qt # (the suite includes the GUI tests) even though the runtime dependency is # optional. Keep in lockstep with [project.optional-dependencies] gui. - "pyqt6>=6.11.0", + "pyside6-essentials>=6.10.1", + "pyqtgraph>=0.14", "superqt>=0.8.2", # The evo extra, so the 'cma-es' retriever is tested; lockstep with [evo]. "evosax>=0.2.0", @@ -120,6 +123,7 @@ testpaths = ["tests", "croak"] addopts = "-ra --strict-markers --strict-config --doctest-modules" filterwarnings = ["error"] xfail_strict = true +qt_api = "pyside6" [tool.ruff] target-version = "py314" diff --git a/tests/conftest.py b/tests/conftest.py index f02134c..dd00fb1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -389,7 +389,7 @@ def isolated_settings(request): """ if "gui" not in request.path.parent.parts[-1:]: return - from PyQt6.QtCore import QSettings + from PySide6.QtCore import QSettings tmp_path = request.getfixturevalue("tmp_path") QSettings.setPath( diff --git a/tests/gui/test_base.py b/tests/gui/test_base.py index 13f5dc8..d91ccdc 100644 --- a/tests/gui/test_base.py +++ b/tests/gui/test_base.py @@ -4,7 +4,7 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak.gui import settings from croak.gui.base import Stage diff --git a/tests/gui/test_branding.py b/tests/gui/test_branding.py index eb737b4..6ab0a91 100644 --- a/tests/gui/test_branding.py +++ b/tests/gui/test_branding.py @@ -8,7 +8,7 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak.gui import branding diff --git a/tests/gui/test_canvas.py b/tests/gui/test_canvas.py index f504fd7..b341e4a 100644 --- a/tests/gui/test_canvas.py +++ b/tests/gui/test_canvas.py @@ -7,7 +7,7 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak.gui.canvas import MplCanvas, check, combo, dspin, spin, with_toolbar diff --git a/tests/gui/test_gui_package.py b/tests/gui/test_gui_package.py index 76472ba..2ed4a6a 100644 --- a/tests/gui/test_gui_package.py +++ b/tests/gui/test_gui_package.py @@ -7,7 +7,7 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") import croak.gui as gui from croak.gui import wizard as wizard_mod @@ -33,7 +33,7 @@ def test_main_delegates_to_run_wizard(monkeypatch): def test_run_wizard_without_qt_says_how_to_install(monkeypatch): # A None entry in sys.modules makes the import raise ImportError. - monkeypatch.setitem(sys.modules, "PyQt6.QtWidgets", None) + monkeypatch.setitem(sys.modules, "PySide6.QtWidgets", None) with pytest.raises(ImportError, match=r"croak\[gui\]"): gui.run_wizard([]) diff --git a/tests/gui/test_help.py b/tests/gui/test_help.py index 2d5b122..0bae037 100644 --- a/tests/gui/test_help.py +++ b/tests/gui/test_help.py @@ -4,9 +4,9 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QCheckBox, QFormLayout, QGroupBox, diff --git a/tests/gui/test_mirror_stack.py b/tests/gui/test_mirror_stack.py index 8f9beb9..5bf0c03 100644 --- a/tests/gui/test_mirror_stack.py +++ b/tests/gui/test_mirror_stack.py @@ -5,7 +5,7 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak import dispersion, mirrors from croak.gui import mirror_stack as ms diff --git a/tests/gui/test_panel_pages.py b/tests/gui/test_panel_pages.py index 028251b..c6e5ebf 100644 --- a/tests/gui/test_panel_pages.py +++ b/tests/gui/test_panel_pages.py @@ -5,7 +5,7 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak import plotting from croak.forward import maketrace diff --git a/tests/gui/test_settings.py b/tests/gui/test_settings.py index d4a0cda..08d4e1c 100644 --- a/tests/gui/test_settings.py +++ b/tests/gui/test_settings.py @@ -4,9 +4,9 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtWidgets import QWidget +from PySide6.QtWidgets import QWidget from croak.gui import settings diff --git a/tests/gui/test_stage_dispersion.py b/tests/gui/test_stage_dispersion.py index 5c2cbd6..1d72a10 100644 --- a/tests/gui/test_stage_dispersion.py +++ b/tests/gui/test_stage_dispersion.py @@ -5,7 +5,7 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak import materials from croak.forward import maketrace diff --git a/tests/gui/test_stage_load.py b/tests/gui/test_stage_load.py index e94ca6c..cc7c426 100644 --- a/tests/gui/test_stage_load.py +++ b/tests/gui/test_stage_load.py @@ -10,9 +10,9 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtWidgets import QFileDialog, QPushButton +from PySide6.QtWidgets import QFileDialog, QPushButton from croak.gui import stage_load from croak.gui.stage_load import StageLoad diff --git a/tests/gui/test_stage_marginal_check.py b/tests/gui/test_stage_marginal_check.py index b9dd64d..48f4695 100644 --- a/tests/gui/test_stage_marginal_check.py +++ b/tests/gui/test_stage_marginal_check.py @@ -11,7 +11,7 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak.gui import stage_marginal_check from croak.gui.stage_marginal_check import StageMarginalCheck, _peak_norm diff --git a/tests/gui/test_stage_preprocess.py b/tests/gui/test_stage_preprocess.py index c499a0c..9bb12fc 100644 --- a/tests/gui/test_stage_preprocess.py +++ b/tests/gui/test_stage_preprocess.py @@ -8,9 +8,9 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6 import sip +import shiboken6 from croak.gui import stage_preprocess from croak.gui.stage_load import seed_preproc_defaults @@ -190,7 +190,7 @@ def test_stage_preprocess_discard_worker_tolerates_destroyed_workers(loaded): loaded.state.preproc, loaded.state.load_data, 1, parent=loaded ) loaded._workers.append(worker) - sip.delete(worker) # destroyed with its stage before the queued slot ran + shiboken6.delete(worker) # destroyed with its stage before the queued slot ran loaded._discard_worker(worker) assert loaded._workers == [] # a worker no longer tracked (and still alive) is just scheduled for deletion @@ -198,7 +198,7 @@ def test_stage_preprocess_discard_worker_tolerates_destroyed_workers(loaded): loaded.state.preproc, loaded.state.load_data, 2, parent=loaded ) loaded._discard_worker(other) - assert not sip.isdeleted(other) + assert shiboken6.isValid(other) def test_stage_preprocess_baseline_toggle_gates_its_controls(stage): diff --git a/tests/gui/test_stage_retrieve.py b/tests/gui/test_stage_retrieve.py index 96bbaf6..041e405 100644 --- a/tests/gui/test_stage_retrieve.py +++ b/tests/gui/test_stage_retrieve.py @@ -15,9 +15,9 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtWidgets import QFileDialog +from PySide6.QtWidgets import QFileDialog from croak.gui import stage_retrieve from croak.gui.stage_load import seed_preproc_defaults diff --git a/tests/gui/test_stage_simulated.py b/tests/gui/test_stage_simulated.py index 2434ddd..d347c30 100644 --- a/tests/gui/test_stage_simulated.py +++ b/tests/gui/test_stage_simulated.py @@ -10,9 +10,9 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtWidgets import QFileDialog +from PySide6.QtWidgets import QFileDialog from croak.gui import stage_simulated from croak.gui.wizard import Wizard diff --git a/tests/gui/test_stage_synthetic.py b/tests/gui/test_stage_synthetic.py index 7d46024..83d2d81 100644 --- a/tests/gui/test_stage_synthetic.py +++ b/tests/gui/test_stage_synthetic.py @@ -5,7 +5,7 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from matplotlib.figure import Figure diff --git a/tests/gui/test_stage_uncertainty.py b/tests/gui/test_stage_uncertainty.py index c23d396..35552e5 100644 --- a/tests/gui/test_stage_uncertainty.py +++ b/tests/gui/test_stage_uncertainty.py @@ -6,9 +6,9 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtCore import QObject, Qt, pyqtSignal +from PySide6.QtCore import QObject, Qt, Signal from croak.forward import maketrace from croak.grid import Grid @@ -214,9 +214,9 @@ def test_save_writes_result_options_and_the_figure_if_drawn( class _FakeWorker(QObject): """Stands in for UncertaintyWorker: emits what the test scripts on start().""" - progress = pyqtSignal(int, int, float) - finished_ok = pyqtSignal(object) - failed = pyqtSignal(str) + progress = Signal(int, int, float) + finished_ok = Signal(object) + failed = Signal(str) script: list = [] instances: list = [] diff --git a/tests/gui/test_state.py b/tests/gui/test_state.py index a8d8b93..02b7aaf 100644 --- a/tests/gui/test_state.py +++ b/tests/gui/test_state.py @@ -4,7 +4,7 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak.gui.state import WizardState diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index 9ba573b..a0d6fec 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -4,9 +4,9 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtGui import QValidator +from PySide6.QtGui import QValidator from croak.gui.widgets import RangeControl, SciSpinBox, ValueControl diff --git a/tests/gui/test_wizard.py b/tests/gui/test_wizard.py index de0ab19..16089c4 100644 --- a/tests/gui/test_wizard.py +++ b/tests/gui/test_wizard.py @@ -12,9 +12,9 @@ import numpy as np import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtWidgets import QFileDialog, QMessageBox +from PySide6.QtWidgets import QFileDialog, QMessageBox from croak import save from croak.gui import stage_preprocess, wizard diff --git a/tests/gui/test_wizard_flow.py b/tests/gui/test_wizard_flow.py index 1be9f7d..554400a 100644 --- a/tests/gui/test_wizard_flow.py +++ b/tests/gui/test_wizard_flow.py @@ -1,4 +1,4 @@ -"""Offscreen smoke tests for the PyQt6 wizard. +"""Offscreen smoke tests for the PySide6 wizard. Headless (``QT_QPA_PLATFORM=offscreen``); drive a synthetic dataset through Load → Marginal check → Preprocess → Retrieve → Dispersion. Smoke-level, not @@ -19,9 +19,9 @@ import croak -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") -from PyQt6.QtCore import QSize, Qt +from PySide6.QtCore import QSize, Qt from croak.gui import settings from croak.gui.base import CONTROLS_MIN_WIDTH @@ -449,7 +449,7 @@ def test_new_session_clears_previous_data(qtbot, experiment_h5, experiment): def test_solver_combo_lists_all_algorithms(qtbot): - from PyQt6.QtWidgets import QComboBox + from PySide6.QtWidgets import QComboBox w = Wizard() qtbot.addWidget(w) @@ -485,7 +485,7 @@ def test_every_stage_help_entry_maps_to_a_control(qtbot): def test_help_dialog_builds_and_shows(qtbot): """The header '?' opens a non-empty summary dialog for the current stage.""" - from PyQt6.QtWidgets import QTextBrowser + from PySide6.QtWidgets import QTextBrowser from croak.gui.help import HelpDialog @@ -504,7 +504,7 @@ def test_help_dialog_builds_and_shows(qtbot): def test_load_options_refreshes_controls(qtbot, tmp_path): """'Load saved…' must update the on-screen controls, not just the params.""" - from PyQt6.QtWidgets import QComboBox, QSpinBox + from PySide6.QtWidgets import QComboBox, QSpinBox from croak import save @@ -535,7 +535,7 @@ def test_load_options_refreshes_controls(qtbot, tmp_path): assert w.state.retrieve.reltol == pytest.approx(1e-6) assert w.state.retrieve.reg_spectrum == pytest.approx(0.25) # the λ-spectrum control is present in the Regularisation group - from PyQt6.QtWidgets import QDoubleSpinBox + from PySide6.QtWidgets import QDoubleSpinBox dspin_values = {round(s.value(), 3) for s in stage.findChildren(QDoubleSpinBox)} assert 0.25 in dspin_values @@ -1073,7 +1073,7 @@ def test_load_reverse_trace_mirrors_delay_columns(experiment_h5, experiment): def test_load_reverse_trace_checkbox_sets_the_param(qtbot, experiment_h5, experiment): """The Load stage's checkbox writes ``LoadParams.reverse_trace`` live.""" - from PyQt6.QtWidgets import QCheckBox + from PySide6.QtWidgets import QCheckBox w = Wizard() qtbot.addWidget(w) @@ -1672,7 +1672,7 @@ def test_stage6_combine_estimates(qtbot): def test_stage6_save_appends_uncertainty(qtbot, tmp_path, monkeypatch): """The stage-6 Save writes the intervals (and bands) into result.h5.""" - from PyQt6.QtWidgets import QFileDialog + from PySide6.QtWidgets import QFileDialog import croak from croak.maths import wlfreq @@ -1853,7 +1853,7 @@ def test_widget_labels_escape_literal_ampersands(qtbot): Escaping it as ``&&`` fixes the rendering, and :func:`croak.gui.help._plain` undoes the escape so the (unescaped) HELP keys still resolve to their controls. """ - from PyQt6.QtWidgets import ( + from PySide6.QtWidgets import ( QCheckBox, QFormLayout, QGroupBox, @@ -2230,7 +2230,7 @@ def test_app_icon_loads_at_every_size(qtbot): def test_apply_identity_names_the_application(qtbot): """Name, display name and window icon reach the running QApplication.""" - from PyQt6.QtWidgets import QApplication + from PySide6.QtWidgets import QApplication from croak.gui.branding import APP_NAME, ORGANISATION_NAME, apply_identity @@ -2277,7 +2277,8 @@ def test_importing_croak_gui_does_not_import_qt(): import subprocess code = ( - "import croak.gui, sys; print(any(m.startswith('PyQt6') for m in sys.modules))" + "import croak.gui, sys; " + "print(any(m.startswith('PySide6') for m in sys.modules))" ) out = subprocess.run( # noqa: S603 — sys.executable and a literal, nothing untrusted [sys.executable, "-c", code], capture_output=True, text=True, check=True @@ -2495,7 +2496,7 @@ def test_settings_file_lives_under_the_redirected_path(tmp_path): def test_new_window_is_clamped_to_the_screen(qtbot): """With nothing saved, a fresh window fills at most 90 % of the screen.""" - from PyQt6.QtWidgets import QApplication + from PySide6.QtWidgets import QApplication screen = QApplication.primaryScreen() assert screen is not None @@ -2510,7 +2511,7 @@ def test_new_window_is_clamped_to_the_screen(qtbot): @pytest.mark.parametrize("layout_min_width", [0, 920]) def test_window_geometry_round_trips_and_is_clamped_on_restore(qtbot, layout_min_width): """Closing saves geometry; restoration also clamps oversized saved geometry.""" - from PyQt6.QtWidgets import QMainWindow + from PySide6.QtWidgets import QMainWindow w = Wizard() qtbot.addWidget(w) @@ -2585,7 +2586,7 @@ def test_control_column_width_is_shared_and_remembered(qtbot): def test_no_stage_needs_more_than_a_small_display(qtbot): """No page (or widget on it) demands a window wider or taller than 1366x768.""" - from PyQt6.QtWidgets import QWidget + from PySide6.QtWidgets import QWidget w = Wizard() qtbot.addWidget(w) @@ -2608,7 +2609,7 @@ def test_marginal_check_canvases_share_a_vertical_splitter(qtbot): def test_collapsible_strip_folds_and_remembers_its_state(qtbot): - from PyQt6.QtWidgets import QLabel + from PySide6.QtWidgets import QLabel strip = collapsible("Read-out", QLabel("x"), settings_key="test/strip") qtbot.addWidget(strip) @@ -2740,7 +2741,7 @@ def test_retrieve_readout_collapse_frees_canvas_height(qtbot): def test_retrieve_save_writes_the_full_overview_pdf(qtbot, tmp_path, monkeypatch): - from PyQt6.QtWidgets import QFileDialog + from PySide6.QtWidgets import QFileDialog from croak.gui import stage_retrieve as sr diff --git a/tests/gui/test_worker.py b/tests/gui/test_worker.py index 03649f4..6d91f09 100644 --- a/tests/gui/test_worker.py +++ b/tests/gui/test_worker.py @@ -9,7 +9,7 @@ import pytest -pytest.importorskip("PyQt6") +pytest.importorskip("PySide6") from croak.gui import worker as worker_mod from croak.gui.worker import ( diff --git a/tools/make_docs_images.py b/tools/make_docs_images.py index b5f0578..9892d65 100644 --- a/tools/make_docs_images.py +++ b/tools/make_docs_images.py @@ -150,7 +150,7 @@ def _wait_for(signal, timeout_ms: int = 120_000) -> None: it the way the application does rather than by sleeping. This is the no-pytest-qt equivalent of ``qtbot.waitSignal``. """ - from PyQt6.QtCore import QEventLoop, QTimer + from PySide6.QtCore import QEventLoop, QTimer loop = QEventLoop() signal.connect(loop.quit) @@ -172,7 +172,7 @@ def make_gui_screenshot(path: Path) -> Path: session is used deliberately — it carries the known ground truth, so the read-out's ``truth`` row is populated and the plot gains its truth overlay. """ - from PyQt6.QtWidgets import QApplication + from PySide6.QtWidgets import QApplication from croak.gui.wizard import Wizard diff --git a/uv.lock b/uv.lock index 6342245..6b30339 100644 --- a/uv.lock +++ b/uv.lock @@ -326,7 +326,8 @@ evo = [ { name = "evosax" }, ] gui = [ - { name = "pyqt6" }, + { name = "pyqtgraph" }, + { name = "pyside6-essentials" }, { name = "superqt" }, ] @@ -335,8 +336,9 @@ dev = [ { name = "codespell" }, { name = "evosax" }, { name = "poethepoet" }, - { name = "pyqt6" }, + { name = "pyqtgraph" }, { name = "pyright" }, + { name = "pyside6-essentials" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-qt" }, @@ -367,7 +369,8 @@ requires-dist = [ { name = "numpy", specifier = ">=2.4.6" }, { name = "optimistix", specifier = ">=0.1.0" }, { name = "optmat", extras = ["gas"], specifier = ">=0.8.1" }, - { name = "pyqt6", marker = "extra == 'gui'", specifier = ">=6.11.0" }, + { name = "pyqtgraph", marker = "extra == 'gui'", specifier = ">=0.14" }, + { name = "pyside6-essentials", marker = "extra == 'gui'", specifier = ">=6.10.1" }, { name = "scipy", specifier = ">=1.17.1" }, { name = "superqt", marker = "extra == 'gui'", specifier = ">=0.8.2" }, { name = "tomli-w", specifier = ">=1.2.0" }, @@ -380,8 +383,9 @@ dev = [ { name = "codespell", specifier = ">=2.4.3" }, { name = "evosax", specifier = ">=0.2.0" }, { name = "poethepoet", specifier = ">=0.49.0" }, - { name = "pyqt6", specifier = ">=6.11.0" }, + { name = "pyqtgraph", specifier = ">=0.14" }, { name = "pyright", specifier = ">=1.1.390" }, + { name = "pyside6-essentials", specifier = ">=6.10.1" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-cov", specifier = ">=6.0" }, { name = "pytest-qt", specifier = ">=4.5.0" }, @@ -1579,46 +1583,15 @@ wheels = [ ] [[package]] -name = "pyqt6" -version = "6.11.0" +name = "pyqtgraph" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyqt6-qt6" }, - { name = "pyqt6-sip" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/47/b25c13eca5bebc6505394d0223e46d7ebf0c57dcac2ed908d7d19b18ab6b/pyqt6-6.11.0.tar.gz", hash = "sha256:45dd60aa69976de1918b5ced6b4e7b6a25abd2a919ecef5fd5826ecc76718889", size = 1087430, upload-time = "2026-03-30T09:16:13.543Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/44/fcd3dd3f64c83c96bf9bce76ec16cca64bd9b91702c3d08fd8e3dafc73d9/pyqt6-6.11.0-cp310-abi3-macosx_10_14_universal2.whl", hash = "sha256:f7100bc7f72b12581ec479a733f4ad11b8002668e6786e8a445ab6f4d1c743d4", size = 12429735, upload-time = "2026-03-30T09:16:03.713Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a0/bd1399740dfa80c0a94d20b02d89962a31458233dcf70eaa09bfbccf3d0f/pyqt6-6.11.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8555277989fa7d114cb3c3443fd261d566909f7268ceedd41d93a5f02d37ec05", size = 8334632, upload-time = "2026-03-30T09:16:06.066Z" }, - { url = "https://files.pythonhosted.org/packages/d3/db/425b184ac2430ba1978bb507ffd285ec007a872644e2ae5df13332dbcb05/pyqt6-6.11.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:0734959955adde095af9a074213a7f73386d1bbbddfc27346b4c0621641a692e", size = 8321484, upload-time = "2026-03-30T09:16:08.135Z" }, - { url = "https://files.pythonhosted.org/packages/6f/85/dd9f03d78d87460e109e0121cd6201c5802bdd655656bf2780e964870fea/pyqt6-6.11.0-cp310-abi3-win_amd64.whl", hash = "sha256:bd11b459c54dca068e988a42cf838303334f0d441b9d16d92ae6719fcb5ac6ba", size = 6844358, upload-time = "2026-03-30T09:16:09.766Z" }, - { url = "https://files.pythonhosted.org/packages/cd/75/970b041bde4372cc6739c5ef9db1de83a6b36e788e4992e598baa35b2255/pyqt6-6.11.0-cp310-abi3-win_arm64.whl", hash = "sha256:b6324e3501b19b4292c7a55b1f22e82d3e80e519e383ce4fe79b4a754c6f0288", size = 5933984, upload-time = "2026-03-30T09:16:11.817Z" }, -] - -[[package]] -name = "pyqt6-qt6" -version = "6.11.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/cb/ef930289bcd3b4be77a619d6b89ccdd0f53d753053a45f69ed590fd49777/pyqt6_qt6-6.11.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:694486f18b7ab9b1edcdb50e0c9f5cb726e096107da90ae7b0e810f18e2002b3", size = 70402836, upload-time = "2026-05-15T11:18:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/09/7d/d016af2de1975a0d90c9a911e3d82b2e8c8fe899f8af746ade42186f3845/pyqt6_qt6-6.11.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd05b31a3c83111b6eb82bb472ccfe531ef823f70d085b82fd1edc5ad2553c54", size = 64167714, upload-time = "2026-05-15T11:18:48.848Z" }, - { url = "https://files.pythonhosted.org/packages/e1/be/21d0df9bde717131f4245f8801676120d466afe198c4641c9e4982cf85fe/pyqt6_qt6-6.11.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:254af349e0ef4b2fa581f86ee9d65eb797bb1d4f0c01ae5ceaa7b2446b458be9", size = 85897589, upload-time = "2026-05-15T11:19:21.864Z" }, - { url = "https://files.pythonhosted.org/packages/08/69/f6b9cafceed9790c62a7ca3044562d38545bbfcfaf222846482ac2f9bd56/pyqt6_qt6-6.11.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:039b1ab619d63d06a87cacf84b581a2b61a30c9ad5bb677553e738afe4dc433a", size = 84996419, upload-time = "2026-05-15T11:19:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f1/70e83c23bf897c7f5025aa100482f482038ef70232dc27b407659d941fbf/pyqt6_qt6-6.11.1-py3-none-win_amd64.whl", hash = "sha256:7486c80512e823f2d3087e67f854f0556b345f4368040a853c8dc4d30fd3fe69", size = 78416766, upload-time = "2026-05-15T11:20:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/93/b0/9183ec9c206a0c3cba5719f0911e88a3486167137456a0f9318f07ce000d/pyqt6_qt6-6.11.1-py3-none-win_arm64.whl", hash = "sha256:120efbedf833e5bbbc3d64ebdb139b44ff34e60f34410c7b1c2c26d230380bda", size = 59961004, upload-time = "2026-05-15T11:20:49.065Z" }, + { name = "colorama" }, + { name = "numpy" }, ] - -[[package]] -name = "pyqt6-sip" -version = "13.11.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/24/a753e1af94b9ae5b2da63d4598457308da3cdbf0838c959381db086ccc86/pyqt6_sip-13.11.1.tar.gz", hash = "sha256:869c5b48afe38e55b1ee0dd72182b0886e968cc509b98023ff50010b013ce1be", size = 92574, upload-time = "2026-03-09T13:01:35.418Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/1d/62c633faedef5bb3b8c7486a72e8a6466adaa2a14efcfccf85bb23426748/pyqt6_sip-13.11.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:cb931c1af45294bbe8039c5cfda184e3023f5dc766fc884964010eedd8fd85db", size = 112678, upload-time = "2026-03-09T13:01:28.15Z" }, - { url = "https://files.pythonhosted.org/packages/03/72/5a3d9ffef0caa7e1bc7a35d6300f6099bfccd1d8a485b4320ba20013a2d9/pyqt6_sip-13.11.1-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:353d613129316e9f7eda6bbe821deb7b7ffa14483499189171fd8a246873f9ac", size = 299560, upload-time = "2026-03-09T13:01:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/98/f4/886f901f1e04da717a11e180ba19a9c7fc62da170966d57206006f173bda/pyqt6_sip-13.11.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcadd68e09ee24cdda8f8bfcba52e59c9b297055d2c450f0eb89afa61a8dc31a", size = 321846, upload-time = "2026-03-09T13:01:29.817Z" }, - { url = "https://files.pythonhosted.org/packages/96/f2/b68fd566f7f86dbb53d933489e70487cabaea0e0161690e4899653bbc7fb/pyqt6_sip-13.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:581e287bf42587593b88b30d9db06ed0fccbf40f345a5bd3ec3f00a5692e2430", size = 55055, upload-time = "2026-03-09T13:01:33.467Z" }, - { url = "https://files.pythonhosted.org/packages/8d/42/efb7ced69f7d1d31eb8f19b2d778aeb182be7e070569d02b9057ac478e3e/pyqt6_sip-13.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:42b62530a9b6a9c6e29c2941b8ab78258652da0aeae4eb1fc9a0631d19a7a7b2", size = 49597, upload-time = "2026-03-09T13:01:34.49Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, ] [[package]] @@ -1634,6 +1607,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" }, ] +[[package]] +name = "pyside6-essentials" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/90/d528f8dd3f9f20712bc719e10c698d27c998e1dd76ed3f33b6fb606d0972/pyside6_essentials-6.12.0-cp310-abi3-macosx_14_0_universal2.whl", hash = "sha256:0867b709a724db28b161227c7d6ecb5ed10cc09226a53542417ac6dee1308239", size = 116002775, upload-time = "2026-10-08T09:09:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/7597d8dcb36aa8315427635ec6ec9d850ffbad4ce78b9118ac854889f2d3/pyside6_essentials-6.12.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:16f4b5e41daf49235ae048458243da9efe5f3d8040f9cc5aa454ce69893d3bd9", size = 84346433, upload-time = "2026-10-08T09:10:02.825Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4e/f4fd4c016719be832ec49d3c87940459e87fd666179a0a199a3998d07916/pyside6_essentials-6.12.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:08c5841063fc1df69b7fa1ee405f539e66576a83fc48004be3e2c5e35ccbfa69", size = 83810102, upload-time = "2026-10-08T09:10:09.13Z" }, + { url = "https://files.pythonhosted.org/packages/03/61/9a7e32a37b224f68506785bfb0f288d516098ec71681782d72b695a6c95d/pyside6_essentials-6.12.0-cp310-abi3-win_amd64.whl", hash = "sha256:c9a95102aa23c1f86516a30d5a9be37552d324eee7e567eb7508973f1218cb42", size = 82153795, upload-time = "2026-10-08T09:10:15.104Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/43afb3d0e7c25dd09f0eb7e44f1283dadb5fa8d8b517249fe3fde02e6702/pyside6_essentials-6.12.0-cp310-abi3-win_arm64.whl", hash = "sha256:c76aa689989bf9bb9b45535e0a81c6733294df720b0eed1308fa30d2606b98cc", size = 62073456, upload-time = "2026-10-08T09:10:20.202Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -1976,6 +1964,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" }, ] +[[package]] +name = "shiboken6" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/45/11536fba5f50a57671eb14441c6098096f3e57074e54c9d7d66039a5831e/shiboken6-6.12.0-cp310-abi3-macosx_14_0_universal2.whl", hash = "sha256:47105e05baf57d35453e07d240bbe59cb4b8d9565377ea5134ff9fb37d4f5c02", size = 479653, upload-time = "2026-10-08T09:10:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/a04fa003b4360e99d8e3fc2829abacf37396ace63ea1cd616402209ec7c6/shiboken6-6.12.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bde565bb0890044b5c7f63808adddede5abc241e9395a2bfed61919884d6240", size = 279268, upload-time = "2026-10-08T09:10:42.982Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/a4415e6218e0dff764fe2b2d30db2fc8f499a08ace07634232ade3f73adc/shiboken6-6.12.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ff72a72b3277b418902d562b987b33a6f6818bed9fe40a49d634d09bc67f620e", size = 276562, upload-time = "2026-10-08T09:10:44.25Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b6/bee97e7a2edeab72aafabb46ee5adc939790a638d107860b4cad7a20e407/shiboken6-6.12.0-cp310-abi3-win_amd64.whl", hash = "sha256:a1906cb8116869178c64b6bab52623bbc8b4ceed7b30bfc3c9aa0f6e67f6438e", size = 1265476, upload-time = "2026-10-08T09:10:45.627Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d5/5894d35f8a470ca8acff0d1cd8f15b1c94cbfdfe0ce4a6ec0fcf10b95bd1/shiboken6-6.12.0-cp310-abi3-win_arm64.whl", hash = "sha256:da382e68f0815b31b6dc2bce11a465b148c4f42c37b91546a361de1576567d10", size = 1838851, upload-time = "2026-10-08T09:10:47.123Z" }, +] + [[package]] name = "simplejson" version = "4.1.1" From bb0dfae8314facad9d6b6519fd110110330a1dd7 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 21:33:20 +0000 Subject: [PATCH 02/41] Add the redesigned GUI's foundations theme: a palette of named colours, one style sheet built from it and addressed by widget role, and IBM Plex Sans carried under the OFL. plots: white pyqtgraph line and map views with a crosshair, hover readouts, a signed colour map and a home view. sheet: folding sections, rows keyed to the stages' help, find, the narrow-window reflow and the ready line, advice box, derived numbers, segments and progress strip. workflow: the Data/Prepare/Retrieve/Analyse route as pure rules. Nothing uses them yet; the window moves onto them stage by stage. Co-Authored-By: Claude Opus 5.5 --- croak/gui/assets/chevron.svg | 6 + croak/gui/assets/fonts/IBMPlexSans-Medium.ttf | Bin 0 -> 202460 bytes .../gui/assets/fonts/IBMPlexSans-Regular.ttf | Bin 0 -> 200500 bytes .../gui/assets/fonts/IBMPlexSans-SemiBold.ttf | Bin 0 -> 202632 bytes croak/gui/assets/fonts/LICENSE.txt | 93 +++ croak/gui/plots.py | 566 ++++++++++++++++++ croak/gui/sheet.py | 501 ++++++++++++++++ croak/gui/theme.py | 340 +++++++++++ croak/gui/workflow.py | 187 ++++++ pyproject.toml | 5 +- tests/gui/test_plots.py | 283 +++++++++ tests/gui/test_sheet.py | 226 +++++++ tests/gui/test_theme.py | 86 +++ tests/gui/test_workflow.py | 106 ++++ 14 files changed, 2397 insertions(+), 2 deletions(-) create mode 100644 croak/gui/assets/chevron.svg create mode 100644 croak/gui/assets/fonts/IBMPlexSans-Medium.ttf create mode 100644 croak/gui/assets/fonts/IBMPlexSans-Regular.ttf create mode 100644 croak/gui/assets/fonts/IBMPlexSans-SemiBold.ttf create mode 100644 croak/gui/assets/fonts/LICENSE.txt create mode 100644 croak/gui/plots.py create mode 100644 croak/gui/sheet.py create mode 100644 croak/gui/theme.py create mode 100644 croak/gui/workflow.py create mode 100644 tests/gui/test_plots.py create mode 100644 tests/gui/test_sheet.py create mode 100644 tests/gui/test_theme.py create mode 100644 tests/gui/test_workflow.py diff --git a/croak/gui/assets/chevron.svg b/croak/gui/assets/chevron.svg new file mode 100644 index 0000000..c3a423c --- /dev/null +++ b/croak/gui/assets/chevron.svg @@ -0,0 +1,6 @@ + + diff --git a/croak/gui/assets/fonts/IBMPlexSans-Medium.ttf b/croak/gui/assets/fonts/IBMPlexSans-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a3826c4e9ab3f2d6e64e0c9aea7122ea3431e174 GIT binary patch literal 202460 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%0Ddh{R*&}GNDKH5ex->IMz32>sP%%N+ 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 literal 0 HcmV?d00001 diff --git a/croak/gui/assets/fonts/IBMPlexSans-Regular.ttf b/croak/gui/assets/fonts/IBMPlexSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bd6817d5202895da5ac4fad88de3da71e652881a GIT binary patch literal 200500 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±Lk;941wN9j(O#6{H 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 VkO`yz&)k7yBj|jf8c=D*002BjV;}$k literal 0 HcmV?d00001 diff --git a/croak/gui/assets/fonts/IBMPlexSans-SemiBold.ttf b/croak/gui/assets/fonts/IBMPlexSans-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..09ec8cdae8208c26165db0c87da409cda8c26ae4 GIT binary patch literal 202632 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

R`tEypug7icv=;;n|9pO|AA77&M-M zUzEt27H$~ioyCvUp`4pg&Y)QiS>^iE1bO%9aT!1gKvjdoe$(KFh5WB|kAb>qe0Q<8 z1maFpZ&1VOkO%Yv&M6nr<=f11bb?MU*G;B`+AE>`VgciLiqZ>E9rjY}sItvx%|7Fd z*=LEQ>uN_wYp;`aEk<346Ju-%s7vaq?9BApUo3OZVC zQD|Zc&J_nzWMstEpqLe;2b=>@Ic;V|M&z87l#<-s(v+00$gGMPd~R+{Lq<`)6Cqdf zi!vH&a;bP@7hw9GAc0rHWW{;=PhpCbFogl8SP2ti16VNObOct}(w&yNIX@yZHa0UN ze{*VDH=kSQt|{9n@NZ*Tjk}J<*slKY8mQri?&BH`*q zIsEQNlU3{?@Oo@n(Z+!kQuN~HKf#Y0QX3+KPA8r38Vp~ z4%y-L4W%B8d@Fj_n~n-+(yTiBvdF?|j)=6l#EfWnOyi4@$xcUkQ+9eG!&>7;)5F>o1>?Lk<`;=HF3!YU7LGD#4>QA9 zLpll50xlNgfy{j9o-E1Ug@Sg0Gx_mOuA zaA6nM`to)muT|z<=gXVNyNpLL7uvRy$JKB6BL6GrQLJ@!6RbwdrMf1vxndd1dLf z6`5C%2>Sn|9SA99w!=Z|qL6k7h-g!^8g}|4uEw`Xg@{Ak)lRokF zfUi&Pq8^}L`kX$YLzi@)WMjgOg(R8{nJ^ntYc?j|Y~xA%9miid2$CE|Q>08B+GM|> zIz`iu?)Q}BXJpQB?V17sz2%wN&AHB;ehUXz0tZ#b%^^7OqdH#;b>mM$vM0rXnzl1B zIpknQAtMqDh8$WGllg7FlsqZ;PAO7!c16B4ZQqZm_*3?M!wJ{TdP9TVG_Q5c6sbu1D^m>1zyw8*#@I4l#2y^v);iIYi~0cpXSL90(G>79558a-!2|KDl#S(cY~C;rNi<}YG2P8<(f zg~~_3I1eyBB<=yJr9hZnD)}TxGamAjq|q8PakrT`EEA?28n_BF*U2QEK5S$IVgZO7 zuu~eqbOV>ot_x=F+c)!quLZ;X{-OHq+v^Vju_{{)b<=PF9k`Ev_!%4xs zuz$mxjX^MqvuYnJIDcvYIdD6R70dpQGR~s$0(A~>dq_^El1uC9^)8h|!z2Qf5&_O0 z0nQ#F=V{5=Xb@Aci2qaWhR!WACvBg zN(o)WQ&TATH?3^xV0bC$Nl05l7PvL%de1!5du~nX^os0=<`hS1US6pq1*cS0Oy}?H zY+Sjrac4$GNljXDW-k6^7UQs?41-Vnn$H4_(>VOU)(=z_X*bXhG9atj512*m_@9&( z6Tj}?(DjPy2uVjZx+CO#L$t%iEs$K2jIU5%Bxu^9z}=E|bmA_^B*-D!p~G4dKsyPb zodnQMf|3fBAfai85|F7C?Zkt2;w9}Uoni#4qMdudtO)flM0D#dsgLyZ7}-Qlep3o< z6QDU2XVsP!rf1G{?GD#%x<$<$xqWke4KsOSpXQ14%rWa@g*7JOOvw{T^MjEOQVHga z0!l{M)16Q^cO@wxD}sk_-}*cW1q!S%XmYW# z!Jl+(szHKy2<9t01XxnC+abW(mECp%5>EV0rPi-3EUd&I|D-rOBfD5$peNjqzIqLG zNIL!XC(`K$ke#2Zo1mJq=l)MM{elr2DJU@2{VD|%K+pdt==q-p;_Vc1@Rrq6D0JP$y=~SKxzcau3`F_>;K}Kzji1K(G2!)W0B5mqz0@uvlVI zz8ZB!K>(>*mBvwR5xi6KSUo_3{`MX05$JjXX^-mBTLVOZk`+??Fqx1!L^XNHp}hpz z@Y(XUwS^$AsDG@4T8uq}KYNrs0>q-AT zgb%E`_VD+yhwyE%f9ThO_YkJ;ESXE4nn#sVVuP$5iP>4#bCOd^ zAZeu}cSU4X%;fLqR%1iKNgE1IY$#MilR^6l?_)pVTM*=c3j>VKJU52utVLGm4}gSC zb-PLLWV1wk@;}>3ct3b6LGC5I0-ApdxXP#9AH%1mxAqby2_mY0F%pCp5J(WM`+z=X zetQTB0-RW=wbPJwf}IA_9a{Q4h3qzHw=VvQ+-Y#rj)Pn7JhW%0=cQ-M3noeIIlRgq zv5g}|dk#3q=7f6=B(6(bOKF{2rZyII%Y5S?OzuU{2$6$gl7;|Dl*eS^zusk-=Gi&# zd~S zd`vrtQr~$@imPbJQ8TeYC8%T7no6ne%#G<2=rA8E&mtXW-bVEFMY30DsYyPitX~k z#}Ihse-pkhsyYjXPpZyB;A68qj~4mh>q0-}8D9qeV${47qiF6ZWFw8p7$7o6ktBtW zA^fFDevl+hB9f%9s$QBViC}bML;hW0Y;89kptE* zNm##R(pukd{gNEa^>+CbV!NOh3U02gt4xwdZvVu`!JXo3ZzJU7D&tJF7YASx-_11J zD-%SJa2Um(9D)YDMth57d!4U=%JBE!S+gLh2-;XiQi1;}0PVHb2ln6=&pDy4YsVBA z5ch_vyPgX{h17q2VBCwbqx4^HU?WifbzE|s|6OgD*oA>H@%)slI`C8IWIiw+0!&KZ zMLHup^^i}^bsTaU$*XFY_5VNZmN;~%e%rSCLpMwbDXo+JaaVOx-2HE8vTXUD#|jTl z2@JRyt%u)a#j-D>q?S-}Tpy$LvEC^;E-85a8}*dz%Y6z@1j8cm;(Kk2(07^e&(PGg z!JNNnM$^2+H`fs_rs>dM!c&6Xf24Lg&wJKc-t%fIW|U_}Hm9bRJDugJsm+m@IQtJ(NwOnd8fmKExQ+O4 zlCc&r$m8FRl150)`8u&dP+Nt;#wP+8kfPr`CZS^^eISBT_&^0SY1MhV+#*KPDDNCMAiOf2ZS{qxhx= zd)4H!1kQ+W$eP$r^C%R5Qo&EUW&ET&X5~u#e|HkUd2aO5rTYK5|2~)ah;d-@AL3>H z1IV&VjEBgjcd4-q>XP7CP(@_9TI6TBT1^?%ei`#AW4_;k=K3T#>6$Vhx|UWW@&{fla97WLN`wa;)RwbWjZWeKULVLt7L4G75|~<4Ow6*lu*lrqelf*4grng!x9luE?Q`9KQ_R zS+XnScD{eCwjAKrFe1<&fpqL`6+j zi{A>4loO=8jaH&&^fxjoCG(bivWekjnc#Oo2JD2$agXc3IW*uL5Q6i4Ir+#b18d1c zt_=$lI){lI<>O?EoE~{tvycY;gfJDqg^XqGsUBY}4(STbKAcEn@3MC%nzLNRsTox* zrKK~bW!t+W+@&Q|Gg?=a=VfK(VGUKK(;QOPY@HU^!JebjHotGeKpVX}KOBr=r%v0! zGS1QI2ym?zbUM=T7{AeJMB-wY*(3-slxB$bAk(qNVKzgj;KxMgfab`Hju*n=m6&%+UGaMuAwV({|%#v0JAR$;XzV(~-tz8!6JL zu4tpycBM|o7)7=p>vSx0+TPRYIKvwrtOF*$<`S4GH9Uf3v}9Mc8P4TBL!-{+JK6^Omi2ELAM6?NRJ&YtGw3~C z=o|W^N`A7ce`IuEc*rTg33+Rl`Az4*sI$j8Hqz7AzoTblyK{K651LAVuw`I$tbe4x z&p9yU+|e_%$GL5MWMH&!pmz*qk2-e`j|}$h9_Z_@ZW|u#b1obn>g(CFaA14Cvvt#^ zfuZe{&LutLgU($&L(abb5odQ#&k(W(`cbCLTsAzqqi3*xXzSS6&gnHZyLa!d-h_g7 z$a1QChj;kB7}&Ifa#m~w#9hNfW6tHno5yzdjPz3z1_yfkheiSC_z+bzwzc26d|tP6 z@y`As^>MfQpwemL#a->LcFMx^ZzU#n^$ZO5Y#Qu$?j9K13dD4^E_L>dO?PTIMtesF zc8-l!j}8o0507lAS=`lq;x{IaWxp-&-!VWq8XefN6U7e>@9rPz?HTQN_JrhY86E5y z-HNshjqlim#_a7M8SWb%8wL1@h}+0b`4?)fh9JI26+bkC{fK=3&ZPh&z2Xg72!4ZZ!t+pVRLbqw=ZW z7=@AD2u!p3jXr#7{&oqz?l;cBH_MG4V+dbSY6nUl#COZ^ZVO!K2k~wMtg0GyyAYac zhVS=g$}Gn$rOL0ARkG$$v|$)EIQ_~|P@L?y!EoM*-w#MAdypCfR6T&R9}p9S+mSbH zY{u36or#-Dr6MaG7^`WFaNDly-e*C7@*o6P6mhFfrCo2T!tD(C9KqILy{uK(DTzVN7n!*`mY+bDTsFX!BdTv4C9wXL!rMIz;AZo zm+G4p_)6ipOO`-Xw;ZX>_~mXuNO&}RVGv*TN?3@#2%qDCi!ehi8be9+?Q---H|~o8 zE5YqFtdhF}N}#p}(~BFB(Vx?>APcP5gr-O0bU@PUCS0j*>M4T53COA5R!qf|9s|~# zLHHO&Td41M;_WDEB$^$>oyOM|V0bZp-~C_x#-vA+)~)fj98m5+JtiH^MQytQ8Bz9L zS@R~OdOSiauvn~B?Q!iR3`N+;fu;T`8Nayaf(sd>qHAfsB;HsOHspKc^?LK45MD6 z?Gt~~gOU{W6P%QymJwzG;Ud09nCZr1M)#9x_^!_7sNcD*;tsd0K~A+SOl_ILQ6uB`3hqt$aghL zTVtFG-Nia%JsmfWaK@!r7nH-?565VuBN0qkvhi9lLoXOA!u)}yeTs5iFH&Kz#V~SSCZXE8|?_Dd@CXf#x`Do?ij8YiMze z+ptfQ2ZH*laV5((e!y~!3s|o4h;gg&b>n*DazyEHvV6pdc-%P13XE?U=R>BxiWRaV zR?JEeyrPVivkEqiRkA8p&1#qn@t|rEFV)NHpdtUhF~I6s1BSvNYc#g8CN>?C@erG7 z?11HRGizb9Su1Np6vYmBa&{pe*jz*roX-}pZnlsuVvE@lwv;Vn%h?J9Jz2$8vs2g_ zb}C!T*0J@*jqEgIf+0*b+rT!m9>$D!jh$>0BbG4A`dGhlI*vEk!nU#jwvBCPgKP&I zV#CHi*-myk8)2htjE%EhY&Sc@c*PiFd)QtK#NBM4G0yh0Guc_}Y<7U1!_H;rv4iY< z_9b=!yO4dEU1a>8UCb_Fml|iV%h(}f4?E1hV(c^av&)UW>yUI9|UCpjx z*Rt!_*Vy&!26luUWjC^$jC=t$_`?~Q5^AmI~?ks`!@TI@ju2{ z>`rzUb{x)OcN=H3d)U3~K6XEQfIY|_Vh^)N*rV(*_FeWk`yP9OJ;}b$o?<^>KQ!KF zKQbO=PqQDhpRk{@pBaC|%HyZ(=j>UWI=hKIXMBepW5?O^MlXB8xRbree!*U1FSA$J ztL&HTSM1k_A9R@g#`p|o6@Bct>~;1Ad(-%n(a(OzesBE5_!)bP{lR#Kz0Ll}-eG^j zQtdtVKKnEKANCjH6ZTj3H}(PhJNwZ1Is1rxY&>f`XMD>3VH{(huz#{o*}vGo*=KBm zO>%=X&bdIer{D1~9*(F^n>lt;cqGo;e%{!^qm37N439;$qOb6H<1QS6lE{;IGEd>D zIPKoS(|HEZH16hEJe%iWee{;`FP_JpJf9cvLSDp+jXxW|zzHIy#!JRlUS_<^%W-DQ zG+v1lIjeaMci}{VTJGUqUdQWs18?L_d^(@OXBzMES-jbJjJNRFyp^{Z2(DvXZG6+X z&bSskq#f`->Ed(vTt1J_=L>i@U&t5n#e4~0%9ruw#@F}?zLKxvtNAH7bK+E->%5My z=cn-vd?WASn{d`mAL7n$=3DqyKESu}?R=2$z-b1#Bb)e@LO@t<~R6l{G0q+{C0i^|2F>)zmwm^@8!;Q}- z!bG^RiwF@ZqC~Wa5wRjp#ES%xD3V07ND-+bO*lll$Pk$#OJs{2kt_0qQ{;;R9EVaQ zibaVi6=kAaRETM!QdEg*Q6pT!Eoy~FctxG47Y(9OG>PeAhL|a4iDuCvW{Xywy3sB= zM5pKybHrRRPs|q!M7LNd7Kz1TiC8L@iREI2SSePC)#4PfMw}|vigjYWI8AI28%2-U zBzi@k=og#C7O_<~j@SnL$1ixDv@#>BYTC3cH5#2({Au~+Of zJ~BQQ`^A~!EOE9tAkGoziu1%lalZJHxIkPezAP>h7mG{8rQ$MiNE{Yl5toZA#8<_Y z;wo{qxJFznt`lDq*NYp(5ph)9C~gur!{YOo;#T8V#%so9FvhtQ3zAojH;rG5uN%J+ z-w?NnZyLWf-Vomsw~IT(x5anFo#HNWx41{#EAA8biwDGm;vw;{ctkuZ9uwci$=BZ# zPlzYQ_r+7<2jYj~N8)MmWAPL5Q}HwLjQF{DRy-$;iR0pV@q&0!{6f4WUKX#2SH&;I zuf(s#YvMQ1U%f8g5O0d#iQkL2#2>`l;*a7T@h9=Fcu%}9{w)4S{6+j#{7rly{w_Wg zABm5}Kg1{EpW;*TFY#~jnV7JT4-L3nt`2)oHCAnKlGze)uZ^M=p31V4wUIJ+Y3ee@G?! zNgts;92(FC66DZOxb)qv@3s0~M|YQ}p;JEBy5)1N%cI|WJUYKAvS(;`tbee7phx9+ z>gC;CtA6Qrt1sQ|dRyoC$S}Qhb$YtwN3K>~ZL3#5s~@{s8})OOzPIXoo4$AGyQ;+1 zrpsy5;I--U+BDd0y1X`BUYjnjO_$Tw72dOH1PG&IUG8>Sth-Zzhr7q#v!kbXWO&Hl zGrR>|vONl`@S!a|z2jr@C-_9R$lcl2Kt9W*-G^w(Guc*L~kE``lGq*A%_Q0w-n*R9-vK*#uD37GK>l zTO?4b9f^KFP&rBdKT}x=){j+&ZR@6;t-a1FCu9 z1F~EBz&889HXo6@YCR3%>M?qoRd&pFzjnlI-vaJ5G}to)MPm4%{B8K4L~qQX-*>h_ zv^RW6y&qEVhy3105B2OE9vvGQ-nq43^bc(bANFs69OVF9CpYysr#?PylXn>Fu&JhxT7I_M703= zK9Ez)h+hlBN9C*NQ9n$L`n@MmaFT9U!j}FKs9*aw4eC*A8$;)XkI53{N40JVughKU z2_N@IgJg2<&Q1+ZXG83G02;=1ua2u;4TF9cm>idV8#Zp?qf@u9OSiYv#E-^Hr^JiP z-66R%?vk70-mV(a(Hg#6HFmev*vQ=`GYa1=8Bg>c>!s?4uDb9&@|Awn!>6k$e6K7n zYOillgtz)N-0b&OO~|da_Ez20R@Ky~)?oImIeMFH0@$(sshTkGtZ}I(B5+38G~D&~ zYC^(utG%^TwYA-9PE@-EXou{$@D8wxp0V&w33hm=%#7;veWqHhrX|$gYVXty=nP;m z8j3azeY=LH-5kN@K#XnVFm-F~}P2 z^E7PpGir`1ev#jM z(Fu}W><6DYDBCq|+B;(x2T=WD1#R?VzrnxQBK8j5u1<|RMa1sT4vphZja!pTDq_cb z^LtGx9rfYM6x7QU)XV&!4qq-`MKAY5$a25;s{5NdZ7XQ3uaE^$o~za^!NXXu4PWWs zYR!5&%`smeyE35FD|H{O)MI_69P2C9SYK&1zf+D~cW0MwW~Ye`jm}Pq28?YrXSqA% zpv1H0K^;xut5w@pTWyP6?HlW>)mUF+z0`x-8@|SWtZPc{Y79SB78iA@k3PeC26t}l ziI!JApKaj~S^gXuD?uYYi?$KJma`!vHtHf)OY2zrTap|Kv@04Dgb?L9kp_F(SW zv8k^|BGuE@juW@}83ULY251@=BU^{VM+dg-=y8Y1n;7dEw~wnb#m=n*q8ER73>)6j zzojRvl>oA}Q9W}A2vus6*WRhYIF-QY#OoCjjClmc0trU91cNUe=ZnVqDFgP!3Ie`l zKrGuj9JZW*5GzpoN?rRB)ZC80C8GnXn%1T$iHfn|q2bX4E2&TuqcI`hMgR<%h$KYF zR1Bre-bwU-P_H#c)Pu%=e2CW%=5JGip5;eT5g8Vm9vUO$*$MlGh!kgp_WydIa7h;diA(7AFJb9p_r@id}vuO1xk9c-JkOuzT& zd{0omSLb_!^3_Q2dg}H2`k?m>I=>+(zftEm2IV*D{HCD%R-NA(l;5WF+k*1th(-J4 zERDMwxn56~{=O^ld#@bvF0WVP$E)$<4Z@FCvKoA+@!<`^hgajntMTCt!iQIJ46j$W z-y76^uWr9LsQq5ues56wy^8aAz3r-9-u9q&>Gpd&_504C_ZlDGAbhB$ugmM!_;5=G zk7wO}w{E{XsQqdwjPG>&-9hbl>-M{ql;m|w=?dTL_PK-F=hpq}4(eaG?q9cVpIi5@ zTS;TyTHT+u8va@hf31eUR>NN#1b?mWzgo$HUEW#^Z>@&6R>NDXW+HE`#(%Abr&hzG z7WnwR#&2y9ewDQB@^-k@DDCiD?oqD)nANk5w@$;}q}$Y`VQVt;HSBe|?REM)Jxh7( zlpy3)61~gYu5r?&%c<9GQIfvP+oap7Bz}CaajQgXm$$A%(NtYWAWf;21Kw+#H0g4i zG;WmQ0OjlQ>U2F&BuJiAr*W&a0WNQ+h7Vzy7Rc9dcIkV)S&oT!vpt%2>NMPSns(|mzUwrc zSUJn~bm;Pwz6JH@a+KD?h*VGwko%T?l+%L|2{>L*>l%cKKcPLqCCD-@Kc`@v<> zf%!X=4m5n78lDFIUP)`91N~ktM?eSW?{qzC`2sr7^(g5MbYRwF(t*CKz% z-?{75@7#6jcWy=hc&~ovR^uGcvc0$~c-(3{;90gCcS%3E%l6~0;BC;#(gv^oPSJa< zTk%8ORXf~@?(wYQQ2Y(g8g50uP#oy`wO+JM&2_bHs{d-+wEDNrrQy(eRJY;}wQjZG z#9g-^A}{D3^=LdOdOb=B(uR-!_-;-Pc z`kv&^{MPST7VP&dzmK!Nr#jzZoHh*m$5={VoA zOg{QRluwENOHi2;B|%k536)IxRjyvi=BlKo1ymcalOT%?1csyy`B9vYO4M&8G00>L zv0?O_d{6~g5U6Jhj5uG1_#*3YTq zBy7nhEPh?#_hSoaNrEml&(yjU$>Xm1og#faYd)xk44yUrQ$qmHn*XUGglEl9YbBq@ zvl`DXHMF`qyL7&i8EaiiX2f@jZ_$Prb&N~NDV<$PS<~6B=F_gOE@_ibwzUzkI6<5Q zoEH!;EsJBcT@8o#10Bgq2UkqT^+&KV;l_^{?4DT$bK+XSif}DqCAgN84Kyr4V5tln zkZxQTu{F4^XB%+cM3&I(Ira-&U&d)n3|0gG!u2yY31er@Q3CDK?-im7{pNs2!j`C>-9#`zZpab3&T;<|yOcI>_P^xJT-@z6GM6o0=!D&r`pyRHB$Wz-m6A$@!vZO8 zJIrZ~Rr0z~Ubo5Xn7p1PuNTVecGvBH4s;3oux8J?}wcXMU!?0br z;@UgEcHM3FN-VRd4ygN&E7bj`h?hw%eKrYy%Vhdo*pzj%y|B!A7h~yDZsT#VkI99N z%c-yyxriSIH6_6IVJ7Ss=E7EC1#Aee0eyTIJN5624{Qmxo5Plb-5lN+-W=W${MBfve;EIELPWv~39lynHsP(r>O^ni^u*bT zn-aGs4keBy?oB+9_@%^eCf=F&Wa0~nZzX=5^lOw$$#+A;1`x`*u<>KpNZzl*G1>#zwy=H$-nbECjX2t zKf{;8`f~Cw`0^!u`6|BrSj?OJ13WLrC!gVaCVz|iX7PPUA24e8WvJ(h$(Q+&$%pyP zcz1`<&cAJV`4f|eQR>Sae2>30`EQi`ZNTuEuuc9_L>TpeC0-Pwa2`0rrJ8BB%AeDzx?pr+dz1yPQ)ME=8;X{EA!{cTz&#)CAoHm5edn5F2g5FJG zd9<54OC#PhUvdG`cpH+TtJj+edQtwvo-p0t-+mXNY+u7`lqOW0rgK&{}dyL$HSKiQ9XRFjO$)W-3!r_ zc62O+&y<4PJbR&+R^{?(LNjl?%mDpHArPDBeX#>Y?MHyMgg z;XIY}mvJW6p`K0Dvz2zm8PxhC8)jDB0Oc{kOuIcI2fQ z32QLDdJ-B)ZrYz&#oN7czJFsMpV7v;9Yrwb#jJF_54aib+ zIDy|q{3b>_kf}OkssS3dB2x{}u>m?3hL?if)yUR0_~E0#@D;Gz0%qI6tR}b#i57M@ zbG`#wJqRY!e5Zr;hUME)Js1YDMQH`YR^wc6N_7~vf#C`;TnmQHVAukN?O@meh84`!kB0q zG}wkUZG{eVk*iX4DnzFW(J9GTDKb`wj1?kdImlQcI+cr@Ekvglqf?8~sinx;6Uf?P zWbIjGtrl5ZfUGUHIuv@RQ-^ibVHI_lK^<6I3w^jLdXPF)q6;nXbU8eIKKMcOBG#%A z-fo~?3#iu;>NOi0)nMnApeJuo$5*N2tJHBmb)18~ti%3oLo#h8RNa)aosE6e=Bvbq~29=z00Zh ztJHfI^`1k$-=f|#srNkUJ&SrTq~5cr_blv&fqB`@M$G@HXy4zz%>s%3*b=!JSu=c1;}V4 zc;0!_YwvD z7Mc2Q?mYl+9_06!$!9LKUJ0H%!BaM>G1jjQ(0UuRZh^+t*sC0Hj-YD`_|~IWE$CG{ zdesWPK6I@{uiDY8T4nhO4psK8xpY{$v6;i;g$Q z7e>podtkwGIjOMtVLkBh&?X5xkk(ajzS+3l+alJFpyEfdXI?>l zWC0RdLf!gMxBgJF50vZ!B@2<#K3I`HXj3he+zKUYp=2$TTn;5`p`<)>2U4(! zde<@!XFSKLL{CJQV3>pba~MuXMmuBIkcC|}RPKr$@_4}5)q!0d*wuqw9oSWaT|L-+ z1ZJDTte)9DQ@zp9zYp{u=-o=Bc01ZL-5ZEk3elQAXiR-N@?QfF@{uTM$QZ2VSbk?C z;}@XiQ=!$RUJ+W*2fkOq_rdVJ4>3hKv}nZ_R)~XeO_oF&uq=-C6tAo^ulFAQs2wlR zVf>NDYo(lpl(PUS@1UG&w6P6std0F}3!2!1Cbl4RwP@n**azo;L&{|DDsWXSQ-Tfc z540Mf)x`bCwqs(;Q?^CH~Tc=(i@lvcpEK*+r5jB(GZK=A838C zzyq+r1AsOhc^Lt;kw6;@w1GeyiM$*Fw9f-=IMDbGJ2jQ&G=f7Txn!>^$yGrv?6u)g zL9PmN^&(e4auw13zAb7b=UQmvT7Mb3EQTf@#ulm`nzTcY#+V-KphpYz;42 zHZ{mi4frm?XEnf|(P2L4;^^JjbN&W?<-)K0*kisKOMy60&SK< zQrX*Nyan14!9m-J{4O@xb@8~x;%5C6O}UuLS;BniwGPg9Ad@MJ^ z;QAuC{t{e&6|O%G*I$H!lFsdLz7Ed60p~x0^B=+a`Eb5Qb%ygbw7Q$&{5mN7I2``~ z3o;jd%Z1Z<`0!?^>^n_?3zu*`HF^#%&w$H+hRZYH^8MJ$8E|<9Tz(ZUKL?kevxxRU zYOszP%#CZXoEp4O4VF@a#neDiNKymIu4+&n%Wl^i)KCNUs3_83lvRV+?xnF`1NPNm zUk&!vU_T%1t7Bgz8C?VRYnlH^`yh_#TO94NY_}3!-9{VocC^X0?GB+$TM&DJeunW| z#8B%0Swowzo&KQ?EKLeYWmO>Pyc(~NAGe(1xCG040qu5$B0`$BVh1 z1kWb(e+tiC3-n+-NMAfiU#$1hSnnh7ApP(l1BeAfJjehnc?sCkrl%IINYH#FXFZtj zhSu%Sw%yuw?L>CMZVYu+AIN99R{!#a=-PNJ-vmm%h^ToYSh#+HW-w73t`$sL@MS45 z3Be=;6I!C26~kP``D*@O!}(eyHKShxc)Ku_b`98o3GpWF7}~&aZUbCvf-458%Q)EOm~xrd?l$Vy}AYq`thTs1v;A5x$4*f$qZpK|I`Nqvf_PZ9M|d%v9eNXMu1TLTq_fZs4; zmEK5$`om@+$8V#FZzIR=BFFDi!?%zMwW-_D#GTZ3NwAY&JN-5tXk99L6RFq)1s)0e zQgi%<`VFk7auvDTXfx!AM#Qz1^pjK~N4zPZEpm1=k5=W4NZ)i?7WZ082617RaM*OIe@ob-wk(R2`%xST3)WmY>% zZO8>`O;FBsB>X{>sTG}a=7XtPH+pw5mS5cRwEkQ5!ma|_3d&q%*uDkkuH3$q+c(x1 zl`D)}!B}O!>J`V>uB1#!&pYH&Kj1;+Iw+>hOX)Jtj$XDLq7ePGMj6U6TDtli4_OsCKEF6ehJt-=S1B@9b2%*HYWb`H}Qvg-B*G9r&Q9gM~O z$Gibhqc@V}dTLnKpL#t{<#c`Pf?pr!swCG8axHKz19D0(gjGp8=Yy7eP#5fS$@vU9-?#FTTvf3o z)7N9=m8LbC_cdUt9^0AZqK^zslsu@c%9sKwqkwUS3)O_wmRfwGezN!I6Q|vUOsE|@i`*;8 zEjehBhJYP&{Yjn(esFwe=^s?`FBgew%)o3HR9*DL_tV2mlAt#y<&GjmyW1{E4j5nZO7=097 zLyBjkf8Q_2P6>LXS*H};|5%N-y8C!Gz13uk9&&d&A64H2+H&bBAhrVzvgqj@WDaM( z9}(wvZ%z*Y6ch>hd3;Z<1=_HVI+;YHcr)zh?^VLQXifBCv<|L65p9J_Tcaf$7qD7J z2>^1uzR^q3e0abHXKH!6!~KiF_k0o#=}p+hn_-&#IiPcnQqh0cZG&!q!^S%%HswvQ z`rF`iExKo#&|h}ITeOqjST^2N^{_?T4jj*DIZ-in5jY|-YP z^a;DR%O#AOk_^!T)h?RH7a8L%YLk}7TVb>q&F_&~)PpxYz(n(i>AmRXXi-$h|G7~U z7NU_iC0Se4fQ+ujiY+64O>fj-55I`kWd-*upil=AUxO?!0pbQFj5ZjyNWG&p{o;eL zIZdQC=}!ZL5+*-w(Qd;@x;LBpVco!K7Z^3uy3N|`&Sr3`#+s#df!y2KV@i=+Ho(XB zZjyb;)>lDG(<<~zuzh=^)!7?$YlTkRDZA0iYs321vTsMXn~^PH+2Lqq7GWp%sW%qL ztXT#Ykak;%cpd1|#J<7o0`WR!G+EsZ5?q1yMEQH!I!SB=tsBv4Emm)y$y1yw+9DaV zZBXHyxy~rnN*#fn-mpE<{iM{&i~5PKCcDv0 z@;)EE5xofKSMi%oT^Aw|e8iVtsY-Z=?orZeWMflwCvvug)^QmWU(9cLv=X1#NedEO zNxca#w~n0hcbnlvEApI3KkxRqy9hiZq-h6zaC_azB^lr5Xu`YN9=4d`!a8e%8+FnP zaxak;$7?p5FCa}!ALqAfspp1h4lPZMK?>2NRni(#QL;90Z-dVfJ?2hZmLfTes9z&I zh$WF6s-1jm zV@dl{yGSFbGWvpKI!GI1NHu9l+sNB9q$NE;B6W-LDDm6!dO6Vt$YTgMK7fz0M@9A< zW7|+h?RH`>8Y4QN}XDw*<8n-Bx5%zp;>y(o2UcW=y%=shFtE= zCQuT)%MSM?ORtjh-i`qR3rI!L5vNPJbFEosE3C(JRb7#EG2J-d?ujFjQ zI_M}V*yuFaFsOI+$daEPio}||hTUI;My2GSa=n!&dxO<erH%pP1W;rOQg3bs2pG>{p@PEc0=zu_tZW z(Ir;MI7X^>>Ahr&-pj-*7khcgM&U-?n(m`_mjoR%MCkM%`JY7PJa|~D=L+zTeRi!{`0a7V^ z&ZWMpmt?u;E%wf&6He9i$(DPEq0JG!Ud6caaSrA!+{o$1GZY}SB`+7&B$sJG} z%R-#e$0RL8dqnZx`;n(=EWmq2E6efHg~W8U>#-MANJK6;RROo28Y0Www;fYDFLFb#1^KG!E9s55f59LzqTZySTxq!@ zipM=Noo#}_a+0Zj6}96lh#1!qMU{}NANoUk42gV=qkN!be1DswCT)YTW1E2_oj3$7 zq$doEINP2ncJ-iTC%YX#$(-`QCFWWT;?GvPi=uQ5~Vv1gggzB=VjXp?& zQ~{(Ko6eIA>Asy3csMZpqVpm8Yl^*{nSnR znQQvl7n?~GY%?bMsdiZQX4OsIIROG(QFVDUr7FX^+P0(sB71DVFJ;t z34heLCYfpEhZN&gn!sqeG{iiYth-t}p|d|p_HJiUTrjar$j^rsOZNk(w~*}J7SqW` z*w%wgW`j#XC+lIN{2C;P4O$73BL3M}idwKWjmyr~?K=3q#yxmXUiQ=t*zvDXpY`3+ z{J;JN)~Nt0HNvk#=*$>sxJ7G`JB`qJ0Iev|FU9jd&}|3SzM8Z3CVaw`$J1dko56<2 z2XE}l&eDq7jKwPByaZ`o;aVK~DLJIIpmvXHflOm_2jN?`qhZbm)*=f_sFitN%A93= zZ?b`(@J2htwKr4r?k+)s-i)4Bsg!*Pa{e4XsxX>Kp*eU}`VANryRQvROK*wai*+J> z-(o^aSd@t*)6f2oUf>(S3C*WDFg2hpn}fdjN;bJx#t<*sxr< z`Y5dl(-!5za<3&{5%&8o{wX$A4|#(U6+*+9m7${+42~>!^XX50UP2#Sw689r#sZBi`F5XSzZ!&AQQr0@*gZhxl1< zi7ejBg767lCXrE(rP4amd#}B1a767o;%$SX`HWecX{an*zPpLW&E-n{WtpuII!0)+ z-mmVUXSN=Dsh;Zww6Tf!p1G800&m@kVNzJn+@4!2@m)M7eE>eWx5cH(_RXZ< zxs((4_@njMlfO@PkiA8~+=m_C*Ru>6KJFDZk>0j_=yFy}k4^NUY9o|MK~rg?wLcUQ zA=4p|iQXuuH(H&FHqx6}$9d%1jjd9Q?)pDHdr#A9??ty_fn{fM?5I{wY;}|-4ahFQ zV>cYuj*qkmT~Qkw9_9_S?d7PoZm7;9r!jj4lrQ2Eb z`_bCb|JaHsjr?%~GS?!=F`q0pv9@J~B3bcD2BJzyHcH(Cbd^}knO-RR?1(}+FJRwl zC5i57nBxOXk&A<^Ez?0O zyMlK0kwpFd?D`mQFgp05dDNN2j&q^XTqi?%djVx)t0WE7e{@4{74+_)g5HBhu$Htd z&R%ZuEj3MVIbir-yjduxP44W0J)&;d2aA6iz)S64_1!sJE|0y#IF^(=Q*G|7?O>u_ zz8C?QK15enz^kp4-lFzDXYsDVaI9ebL_e-7$g8(N9sKLGQ5&cCh142-C9jKTj4#%@ z_Ut_ix$ssdkzCaRYb~<#0cUBsVng(aBmJ_S3a~$frb%EoCGzKk1Wn$?21CFeJDnpqgbi={4>cl?*d<-f3uTIl+Hv_qg0j_2CcWanfbkTNs`wYKwUuV<+t}3Yx6Z ziz^hk$6L|liT!u_>~yz@8m)w0(p=LeX{x0n?&DsI#kzHNOaFy$smY&v<0R=gpw0C1 zfKu)?QC3-!)xD-eR}{5b{o`29rQLfR+cT_;73lf-|K?pZW)*dcE!DnDc6d9R*F#Dl zpU%dUB#v0Dxp7Wfll1BzLBcvnSZh5n%r1oUMP5EKpq@E>IZyFPVeF|oy}wG^J-tyt zF4=8M#{?;0(4yKCXcHd61S-lYtEX%)%L;H2zqS|ns~Y|oprfM^EbA1_noi1XW^aRa-`BHbEC!q{(YiLT3|hk-tWcM11$^&nS& ziXOJ~%c%1br0XH!g6#ehh|ffSL1&qxid9(68$U&VuR&G!F1Zi?{z~6MVKiB#V@+PM z_sF7{2i(`v5w`rJBCVC2^1UPCI866i@T03tcNJ}5*LZKZf^*sj6K7R;BY8`(ocpT1 zMzv_yn~Zg&=P3PQ??)V{v*eyO05p(~O-KDeb@G$KmWYG#`&+1!hkPuxwmZC$p2NfS zuFa&kd2nr>v|O!ZgHI7|%Se+jjxA`5d#fI&NY}*KM)Lsad(h7E2{VTu&x;ZrHwb2h zWSbmKWWkJH_K@0bipz0PMr#~xIQ#A%rH@FL#S`fpoi3=0YZr(%ie(f7qB}}NIU29A z*=e-!x>W(YVX#Bj9r1Vx#nET;yGKl&9Mus|e>^ z4BLjR5W(9ftrwr#Y?OmwU>hl!sF`berCqWla^l`{cZaLhN!**<>3X($V?ny!|6Bee zp=xEU6->8Toy>=EpE7vntcYr%amhw0lhEaf&rb5jIXY21^NcGWNhZD~on)W4FhExs zPV(cV=&5KY9`$V5u~b=`-uCQeLjtU{qy%`^22T~~)iHvhIqp%<$cXqBw>mN)jo!q# z3B}=`QOoL}``h9=Rdc;OMq ziwfOniJxU;VHS29ko!8lPlC3LQRF&?oK91wwhum%#EZ+!ucnwjp;y^rXvSgwKQAOrnWqgUBBgy1S{^m zYjscY#76RTmO!NG39QHiybY;A^InSn3@>$kCSIt9{nhB_z*!U5h4EDMAo7M7 zAECEVmokUT50M^Cxgd%+VTl(R`fGGX0y?B3FC-l?YB8RV{AKd z7V7)>0gdBH^l39*kTE{ld3ZJ-nydkO1Db`6MX(qT%ie&u;w?ULhK&p{k6nO-5c^5C zyKm%?{n`r0ROU9g&>UOzXRVGiU7oBsJC_vI(0&n5vU(;n?=gBRrTe|Ecc55eqjq+E zw)g*uHfB=9*l(cDnI53bO9czM;A;WS1iCs%Cvb28K ztC8sqaOIRBcdzqaVm;Cb#niuu6uk-mp5-@3CEBk3*mzI$IHRA&p*>*ZlV=^A+RB?j zw}o)~F=VzeV(lv3%EaZu71{i2^b%+FfH;fP!u(~W!^@(B6&#h$c>vx8VaF7&#;ps= zL;5>y{TSCX0Ngf$hcqg-B8H>$UutJiZ+84HWfTCDaW;aBrHiEy8Ir%yY#Xx`$h;yB z^+%|md4*|_qBl0mz$@F;d+4tEswA`W%ogXlXL~=L-(y=dz#i#{Z^dds!# zq%3;1l89&TC3Jm4orsQrVt&>=h3g-1Z$bR8vm|ZD@^-#c&$w$gy)EQ8hu;caxh;Ao zx#IrzWZZA&p02PAv_ClGM?S`umiQzO?hoN^@%%_WHR91YbyGy$)+@AE`fgO5)xQg`&Z%AYUsHV|5!_j>#&5f z)^m^tjeO)CsvXtC=O=vuUh;Up~zB$C>O(Uf-cb z^mu}a&PZZEJ4=IdXb*TcQV{>j6;el?c7O}EL;v|lKcueHn^sdA1Qyb2MKb0O)V_ok z1&R#fHpXCjV(E8AKN!;`%{AJo_u4k}N18=n40cqQE^=>H@%H(6icN|XWWxm?nVyT+ z)zzD{&xm1=y#k_)4(rpC-E(od!<6eJy5&lmCu`EV7U}B57l}<2b}qKVzmv0#QKO=i z?lF_W+PCUkic8S5jHjU#*&_L3(E&+zcz`+M$=tIzg(Y)`Ox&Awv+HFnRiXi#WY2p2 zlVn)zu6EDK>j%hLi?s_iJ~o1`n&bBt=MSNs^;aFXzOoD?q_qsY)w|AmTgb6?NE|eXL*zC{|9v4ttJM^ z-Y*gdh2SK5YCL@t?<7Kt%FVVJwtgm_>{hn_14ecM?L5BcT+7#?Z{SPEclh`4jpB#= z8GK{-CB7%T)87;1^Yz+mSTXkozCwFPa1SBfk5l7P_vLswPvw3icSqh0d5iO><~JAI zP#D<+I) z9_$#d$MPG;njL3wJ~P@AoXbj77c#3|B@HOWDev>(}*W9sHG&lk$g!Oqt4O_))D6Mcxc5z-Ur%IpP%xbJ14gi7#z@wh7|nd%)A)ZnD{8R* zNide{anbLCGdQ2gv$LWXf^)roK${Uvi5>_pVK(p7h;RE+>NlBP{_W@qpq?L0L&AdK zYHvtz4R5kKUme1|Vd;BE+f~iCe3Sf65W3&E=md>Xk^N!f*za5T`yP=0Rk zP4DF3+x)KQH;oy(Ds>=Fhi6^&367`U!!{yLo%0LO4|EWdGFpTYS| z))G03RTM5J{Uon1wcP-`KENA7NmZ0oMM+hZ#5ya`fRXXgdpBjVf}Z$drS?>RcaOD{ z(8!yxvTmin9p3L)#qD@_Je>R^;Oj_cWsi<_fn6(DHGxqr7>TPLaCHxuwNTbhFpDT} z6F6*R%?hnKs5LCLer0p$RvYV}SDGA@I8Uozuwq0VrPTqw8R+ds(K9Il{DIsI4-7cYHWpIo(JZ6;Z@vst7U3swl=B_0=MmKBG;)tY z-pBI)41Q;FJ&8H>TD6cBtbtYrv~pYjXA<`&19b}LOThm!ewTCYR>N$yweXrKg|Ed^ z$_;qKn|RxDEBb#M8g@IQtbT`<52r38p!`V51~c$aGwItATY@II@Fti@5|_h;TCjNr z%GXlImywUBk&pRE-%?gfSqjBpXLXVJP<$4cJ%=RDL{?@ZD-}?D02o$*;So^$FeqMy ztW-kr9mq;=WF^mJr2&k!nrI%f(jSZq!MGlbJCK*bV7v)^=>Y2nuxpjR# z8?@U4?drk039ReEx(Te2@@}%TCzc)AlGC7XFF4i<8ux<6y-b%b<~)h@LMF3D*A%Xi zHDqTOHIS^Z`k*l9d@a%;UE2=k?dV&x({McY?a2S{kdxtHHUch>Mte`kW;hOG)wn)` zXJ?XT66?TR$od~ze`o?6HVHtUSD0z?{#3q3PX&4x*c!j^htJhD^iKAHGX3+kV41r7O)9o z+H3(6)<1$Cw4wO-!_ z=+W8gXUjkfGSGoe$e(0e{n_1R0E^}L|M9H-ag@Fax@<>-hk94=hUa=T_XckWnmhs; zm6{D5j{O*c50>_hLjFgyKFw)p>*=g$G?r)M;NBTXuJqz8?9sX0b1@BmnH7atg$az` z#Rj!PJFShZbzCJ+S{v^mXjTEuwEE&=XtoxbZG>i9q1pS;Y$G&V7Srr~XtoSZ)p}7= z8F3n0dN9Ivz`04pN6sTQnJ2WB!O)G#aNrV?4XxDF>K#n(&TIHe*G1{^cxu)Hm3{+h zD>&~$(?0$>yz--318)f>C2Qc_gKfmK(~|ogYxEqC#tnxmBcRquN*~S2MyC^BjKRu} zjqZo{tD)-sU{wuO=R;LuO5)EctbU}J@@j7ww(t}vs@4Cy@1>gi6GLT4h=|08B$S*M6ez?@qp~qaAa}>ao$K`fze37 zX-LNDNXS^u;%m&_daddVVIH$l_9#?DQGo51&dxZeQI9wnrZg0;xhI=p8M-m?auvk325 z1MVwLzTgWo#=4o*%cuiJ{CaZ zIfim~1X*3DqO1&S+D+6C5|p!f!`-UP*0LUG0Kd!Y74lWM33eJY{&A*Oc|v7=g- zcM3WvKXY~LTeR}-URQ7iljUHt6-<_biCPhg(>H;|X0VtI7PG;k1}tWS#aggf9P5@^ z5Q;HA11xhdk=gJB~d5w)KGHXl7P6?OX(o%aID z7@!OU$|U*&Jo*DlfYz6|wKr{^KG?h7Kt7U~wRbQT-+Unu$Ft)11b!EJV}Pzz%trxT ztJ9tX^btTmDa$rUVw`2`eC4DlBTv1cSqn64gkBrKArJdd1c-#BQVEtHY}jdeGg7nKwn6zOKJDB5 zuID$67LjV#O6^*J6auLUNTqQNTd5(fZtn1biEf&Pkp`rZPZ2d`El?nq(u1}Y zh+Ba;(;)T+w;XUoGJvSQkTJ;gSniF(>z%>*OzhdYl*~HBK%5sttKKWLR9w?aZn;-0F-A4mWCx?9mpAAttnQ+^7IrDbm{t?Mh-!s4sODP*wm% zHX@9<&;*oaKv5rRSDp?>u17%ik?5tx8*syMp#W&~ET~0^ES?KJX*tAR^*wNF04nm< z1!5m~>I1PicvVo-LU6+m0iE?hfhc+Nf%rie9Mz}T0Yve(JD!4?&5baIEcp`+CCRH| zj;sW72(F5p9R)5y4)|_NH(Y~jKc`;v>6Oz5dI2_9qik!50`lzI^G={oqYs`+YbFHO z4z{$X|KM&kyr0MWDbjze4ITRU8|%ap>u3(|{BFJeJwnU8z2MDxD-q`YH&&D3t@=Fn zCwU7V@=j(GZ@ymOOF#SF4(Oecpzvk`nrV#Kqq<5q8)umO>2%JbuX_)cJnj51x!d_% z>7Cs9M$i9vi@A<>Ng9_}LO-|Gm|FwQwBBUvUUJMz-?tj2|Lyg{ts1!3$K4)KzD8Q| z1E2N$^k$E@qTJu3aX#R-onN!YA!Kh#L#p@qX}kcr<1IZ~iT9Pjink)(2{K=m5(aXo z$llm*;eP|AXv~1#RTc5Jy3MZmLyJ-4nb`HNxl8G#v}mkMz5FL}yy+b)r8K({eO8MY zFQvCll_e6Kth$Z302K*wpC63j{_0WhgTZI7J z8>Gwp80(;SsL4j&0^S(6rEy4elCqkSguRZA;QfM8H-W@CL*4cabu-dq|Pc+tHRj5R0Wfgt&{ow_5aT|4Rf+BjG`jPHXt4uvMpqGWN=5g{) zr+wBVd+yptv^C0*wmOZDK~EYyR-@s~E84+Z_r-=y2>sL4&B%p~(d{G+RuAD3W>Rr4 zZ589u)}7=!>pgH>1IDI38Qsrlfcsgg4;sS9N5M>MVb$uNiR6 z^wWRID0I_GF&b@J#T#{Ag@UZ))s4l`obq-2SQEr@Y(+M-nxICxybEnLj;EecUwQ+- z)>6#ox*S^7x^Zd-+pTgasBTq3M)crmY=deYq}^=R*7TCG89ShAJ@?u(bL~i%%>7!< z$d1`a3P<@OYBOJ4VBdr#TkO^w1-Hcxba6+193WF7qq{>|To2kiK9F&Gxk+S*>8D zxpF=^WX}w%eUzb*d?yGdc?MfLyvV*b#y7TlMkwzL^inY|bU4_uD5Q$uc3@ygTtin>r zTGV386n}hT8+FtiJ1diNv_?4gc;6efYsB7_f}_@uMP8XRQw9H?!$%cj(MrLS*ol2z zX58q$VIIKK-juTx&1F`FdCyst(u~C8d-uM%+H|dxA}*kG(G)yn+S~3O;>UlI<*k-y zt~Fogw@!|KN5lag`!wXbA+ZX1|{eU%Cm& zB))~$`PJ+e19E;$WX{TTr;H#n#+ zCA_EAV^&_@A;{+^OV!g%03tNESvzkgX?7AJD2CF?NF^Nmdwn_P@l&+GYI_`)+i!!c zd4@*V?OpU^?9$Ht_GmRwvU<6o_9F8)gItL^CN)oTZs(aq1wZJAFz#OSmtN zDn3p@!(ROIsE2wQ#dBG7QIySKTVG51KbWOUUuJ34ic#8l&b~{TIjW}EerKg5bFS~^ zP0N$?11Uc<-7>k{mCY^DmkDfU328pY%o5V3;U(P9+yPdXxg+2&?^J5~uJ0jeZXq*r z6qCvKcg0Qrjrta^z~Ke(S8Ko|d#%l)tEK-~-_9I_MD&J_^d=|zWDxrT!|~np#?4U5 zNF^3bv(=Y%D_?C2+XRhv*KX?jGFNzFcfa#dHFuVllv_v@ucCm3lSb9>wDYP-vJ{rekK)jCUy zk-QA85+IJt#!YTfQ;RZ^r#ig><-93)zJ^CnX;Ewc=Qd4<7mp-q{U_)`QH7SebOmmBrcmep8gp5hf3~rFp3yySRLAx*5XE z59V%MSFX&aB>zc;P3*MwGzXYf8=;=QnI&pzO)!h~IHQ}{8$6&+q(WQq7O@2Jzb3ZX z`(K6loN7GxXE=lfp{iY(0zFrE_mFo_iC$;U23tyhfZqPuECXN;;LjG1JTB zp8lT&^aediiYG}|Ww9kar`Y^vYD`?lwR!<&Fh_U>{i<4Z;yLzeTbZO1dMsi-(cz^g%I3sm@>Xg)!)HOLl&cK{$IlstxBIo&>*K=0qY{|{f z?Ug$`cU11U+|T7s&Alb}j@-L)f027%?(cIS%6&ZdsodvtH{@03-I@1ceqnxje*gT# z^N-ITnLjpvQvS63pXNW5KPUf#{GA1*1w#wYD7d0vdckc4cNP4);DLh23!W{QQ!u|^ zX+d?trh**>?S;98<%RtV4=+5v@RY(c3cpx5vG9t*wMD6-Ba6OJ^tGZ_ixw6wFIrnv zThv_a6&Ds)7FQJyEgn&PdhzFq&o7=*d`6tG+dq+SV<)4iC-a4G_nqtkjHqg4M3u%b zZ)QYQLwH5BB6MFuUmIS{GxsI*b;JXs(7^F{$EmCac@q(&!=jXuG%mG}l1eB^U*y(^ zUyaw+n5R5Ss$|SlDJAKf>zgS_Up23#q~x3H%PC3UT;CAto9mTvNi~cf(~N^kN~&Z8 zRv{&6JaGxoL!gI14uQ&;d(M~JT#hDikLPnx$~9bTd^+#Nu^d&{vr*XFQ}AvXvSQQ+rf@4*1s|s#a!L2GF{Q*dSpoV$S(ubCs zQ<9>5jhEH8tQjjCy#xJLLcb-@Zz=S9p4u)AuZmuSeznwg5w%_ElTMAyqN~gZ}&>Xr%vN8Co-auaq37y zEjZ-Hv&ba#nkA(X$I?;FGimTdxkSE(QL}O=cNh?dP_xG=_wi1$F`%<-7!^m+o(rL6 zJu#<{{~*fj-~?nq<3x`^2960QGEVto?oGm$PWC>HEDS>y zMxjL`G=iBFZqCXPq&|VvBS?KTsfUqT^OTMz^})2O1`@fAGP}Oe`wq08hTPtU4Bw74 z|Cm_wU$I*k!fR&dVU23BMzx{)+I6jy0cKSfc^qrHo#_44Ju{^A*fbxsq#} zzlAPf;9p3|Xa*F}7=PybQHtg$#x@V>a`PuTfLsFP6p#ymTmoc`%hz|O zbAejKem%Zuns*$KE<|o8Q2Qa!>PVy2#avJJ&ZX8vsP$;5djsPIGxoBlE2UxN5>n%= zz}~Ic|9_rnu3aS(-m^8|I~_ztXCFIXCAR;!QF7JY3uHL(|MaUvk?Ia?)h?vE30u{Q zJ?+5mzlBcKV;i=jS6k7mX7p+edbKImD}6=23BB5ZUe#h-^*wuRtIg1DK(88*{ys>5 z_RNk_q@Nj_{Frx)29%;#rDkt4-X!BmGM;MxzU-4oaS-uhE|l15`h5y*EWEPCA2+cQ z2^2sc7GaAQVW;G=HS-lO4MnElsTJ2;Mx=B(`hEqoF0O(iSM!`Xl%|h8Nz(uBy|v)J z)^PdwR|kTrG^uAl{mDMrzkmM!Qi^Q*)H&ApTb>jk<66O0t)mjO-OYf%h`4-Ww8vug z$!I^33VrOSFgxTzq?);)JZ+BSr`<^WHhgD<#BcLb2G7B2C1oosNU8*PC!SSET2^=< zpQ3-QW2o`9Rp{C%@;e-MqDw@GL}-(V#2CG6I0#d`E!QrU?2WKgs-_r+CW9BJr2JE8 z8_~Y9_J=D~)~Jpc_8{JV3?x>!xC|Um!3(?kX>Lv%IU?%075Hu8Bc8T7InVX22w~(Th6q=p6n^fDdG7+)sHsU;kXEDk` z%fT!#D6PAh5+d@nk$!il^5+A2r;Aa5tcY0a1So1%i%b~43UxKFtO+cIp;9%)Wj7hD zwQ)*C_G+JYkV>UB0ILp(bG~K=u;v2`A7S-r$8zqbPFqRcMxC}{Iep+Zk;`Zg1sn!Y z0hpJ9+vNtgg;dR?673zHs;BVOJn|N2k&U8lS@ep?)#e4K9*_Ka7dDKafVA@-hkN_l zD);r!y=~|rMHTnlck{YG)U%OaD?6MvAiTb>p^~$T=#g6$021sJ2ZWL6#W9kDBdv~Pf6eHei3sg=@Ct@Zp*rY zBo0PjXS_ai{E>|F8v?JjDwEdV(HN>iIA2C9av*JseqMl=tTGzv&HO$$dw*xWze*DliDP-R$3 z`(h^$c-N^hww>Xl_wm+U;&~IQb-dprk49!a3xpTbsUN_;*c&3=PF3@sWUG6^n}(Y7 z7su@uIqC6E-b&f~l)3zGqaS-?GA@??jCx97bJiEAJH6SV3@BiuFf^KRA$b?WMU7%> zN5?hBi#JtT*-WtbZM2cAb@nzc{r-t}5xn7b8ztmWqx3Ui=xsWE@4)Aa4)%XljD04R zGVASlya7c$vW&dv%|_=r8oWEsXp7^L<4BU%@;M&yhLhC#wsGljb%$^DL7Ox0@{ru$cCl9lVr;`{eK@g z&GwFLK^E3AM%eQz;uE*|Nk-oJwJ`q9zK+0%LEdfivq*reh3Qn%zk}g^V0XI9k}QMP zvlD%b(8P$@3bHes(d{%8p5*}>YhzHvgUm=AlXGscGP=HYz{rD@JYB$leRHZaE`kyo zKfQQ$QsJjfwos#CDxgbvjzIH7Gpc}J(%}`RjuPlKez+WJV2%Jp0kND7gcCAUwX)_zKOrt z43_$quu{n*v?O-d$tSlfz(r6h&6duC_sAHqksh}hF03LPM^v3-8%eunE-^lcs8C-% zet|OHKz=9_C>iSNd-i>ZP~1D>60k|z4fC2BlO6j6?3HSqhgVq6jF3d;g^tS8SX+}_ zvt+u4R#-nJec-vfiU}GnZPp`qo)l zg+QsH=e>6fL8ns6(h8mqN?q0ycc~iN>i3l4z7L#7jaGt7Hg!o0F7X(|7?(7c93Z6B zdHZ#g*hpGY_t{u~)5XHknA48Bj@PjSX@q|g*Qutwzek!ks&7A=7vREE$73fM@zGI- zucKk_>>7{FD$e|$#aM#pI^mK~*D#CMaJL%8aww^b*CU6ff|di670`t_Vd?5X5lKd3 z({x`%A;)TcbsU-AR_Bx;c#p)w*8@y_RFI@8EdUKG^w17K+45|Am1WD6RvW2kC9&!Ic#N#a%IXVwrCgU{UG?3dP4tUY zbF3jPbIdJuU!G*io!wLW;6o^?p09S=ftB#5zk9+R(YjteE{Xni!pxEZU-F_>t>|!o zU+iN(7nmjGBxgL;DqyYVm-!kW@ip|ZwkGvaUrF4H7xxY%nK=W|QA_h9#G?@1Rhz4o zAN{ZF`hJ`H8oj~LYLjvcf=P?El;{#K>F<7fi4;Km)b3K)`FQu z`=3{Y6q}SNy49>C&)GE2`flNN+WE|5va$EsGm5fw#%Q5lkPX?$r2ER~C*zsumdsAe z>KCybilY>FNRRZ@X{FAA``vjYo2)UtP9|&yk3r~mNMxq-1Ei5ClfFS@66?(7o~u3M zv;zw4#s+J?fxgT{dw@RFEFH?gL&i7YJC@Ox?nYeOw-oHTdI8uvMFs9w)-f&3%uBHv z?PIjMG)HvN26o-(5sun z*Sq>pJJx!q%p37tIM-Utr0lLcSw`79FY9X3B6^ z&?J7AJV_{={MT9Eh;lPOv+riEWx0_SdA|-x45{(PJ#E=k$ZqRo(k3;@+{s+Qi!J;! zt%FPJ(D54lU=8i`W|Kr_(PMv0ov*i6ulvsJGSV&=949s8P@jyxAZC5;S*v|$#xiu0 z#nv-wq*=lrTHdtn78Ls0UbM)$l-}=|>`!AkX7c{T{hkC)3H^17^oXdC^HL})*!Q4O zimcucOwRO(1HTu2R4ZBSQD4vO!}p4wv~SxK^Ite5vL2EbJCfOCskMRa100=B?{6-J zeQk}@8{mGdPY#MZa(@%jzZ42(&!W)7v`(k>HalZ{Ee`>7=@6^0@DsI!6JKiM%}K%Ulns>d>W+$YggD+P8un-6RUjyd7NCE)>LsMutUV`G}L|^K3mo%`R*)eOBzD z`B0g5UyONj0?X`Kh8e3p4k`7IMGr#73cMn-8nI}~ zsnHqb)_*aFH$5})RU`b1p&2ie6Z1AB=Bh*w-iB&A4)s3&VpnC;}+O}l9}`wZrT z>;x9;#*k)aMrzVf=5Rl5B{{eUa_knpU}G~rj%!h;%LcW(@vc*n6rQ>ef2+Bb_I)g* zih z59_UgqWfH^q@6F%5-X%!EumyuqYR4hHyaO48~^jx-_&1>~0oigvq|W zxB=>Nz!sU*cD_wnnpeCS92a(ejxNhP*;`k*>@0y=d#>JTFwpMRi=3%~HV(&AM~taR$3G&o72xCBKK9J1v$olX(*$K0TKRum0qR4Sv1 zk)c_pfw?wU?&%n8Oha3}VuPei_?T_Yia89=?bagvSlD3YvT<1z-2`l{`nHAMbM;~I zwLIP_CAh@4%lI{$FHdgdOM>gI?J_4*Z0z$*qBD2@#w)m@&FZ|!U3WkB$eFBU*|sw4 z_C7A5a~H3eNv!(C?a<6kcU9J@rzGv4vXUK5-gmvrxO0j3dG-subG+gFPUGs+-Z{v= ze5B1ikFT&EY$urSAL^ZH|Bhoc(;?X-ny`GYcw@W~-s$vKR45I8XlJJ=b4;A`IL5)d zO>Oo>tNW8nF`Qd}8=08WsHwW{Zd$eV5K+uRLQS)vD;)%_P1r*o1xU9 zFTWG$s~Z|0d&(cn29Go@(QV~$pm&$VqoMGz+{>izlAzmP-{LyOpRQ&)%I_=2g(~QT z^{{cJ_Mo+W%wzQw*U>Md4PFq}x$F2C_w&+!wy&)$nnSNaHS|#FZhVZaStXd{f``70 zPW&$rIwvj09%IZ#XjANDl$fEAt4_R0M^^j7XJ5~X@s9;?KLzx15@vICU-|y;dY1N2 ze+L47#Vr*-MYf9RKY}meS;^18FFZXwBm6>mUN|9~5?&F0CHzJ>jX9-1;_5c8?+ot_ ze?^-6!w14g!Y9IK!kOWm@b&QRaB;XitPa7e}ApAlqKUEt3Dpi^4pE@*k zWH>D~jJyZ3p8*#8-BTk{Bf!+{6T%6pQ^0wzdul9oP<^`Zs-bp0|4QmCYT9K_os&AJ z>plHXoo~?Ygf4fcCK_#iMeV!nsj11HesVt))c#1;{siRZO4^>C^4z6Gv1oZCb8RB*ui4R9D9+dkbmT?x1MzaNS`up?XPJvB@_ zT))kv>TRwiPYsDIf#Eb{$eyQePW_mi>r+2X-NRWjJH;doX@e`%;p2J8>?262B=l`y zR7)PA*sp*g2}HV(9+MVf&;8Pv!wFCBG~Rv%xq&0l+tGKuqYb!}8Wp#sg4jy?zIh-2 z5H!>GPqU-6)%RuTeSd(C-h+01j9uSp*PpWM$L;!MJNC0XimZKawcQzP$NTIkE9Lut z)Y02**XP-BydBTC<5)YMV#gEhc&Qz))6rXEN40f){|Y<)+Kykf<282tvK>#b<0LzN z+K%!EzW)U~$}9UmeGwdww4?mE&l=piGsupvto3$%m_6mdQ$+>7{|&n{!mfwf@m4!N zZO2>e_=1jQSCoAP%!}zM!6*7Z_5R2IDc`jGwf|fHkN%_acP)R)y#w9vA@_mqzowjh z?Y{+!uKORsYhU}LV11zb4C=S9{aL>1*?oVBFMaN7|DTv{I=|sB1cIZeuogu{Nm0nv z{Et>cVeBxm^27^qCvbdMbm05n*U_7A_vQ8d;3sx;S};x5{wsFp3_JdV-T6Sr(nm|@ z7?+VM@ojB-By?-l=U|5tof^#T78{|SE2_%r=E{_FnR{$hW*U+u5=xA+Zy zGf&!sfUm8V29-ho;Lza6U|2AM^%}+oX9ed3=LZvmsliqJt_!}!w^*lhzB%}D@YCQP z;H!jR1;4gZC@1(W`yVN1rvG~IsFn2q<-Hxuu<}&mmf+doSxU@je~A+Rhy76YHz-xB zM4V!!oKK>~94hSozbzVLIin2G)k4HrN6DU4gzATN{xEc z!*%rT3`ogwJt<$d`$O&6&yJ_ru}Vj8l^vVy&Ja81+VMU+7U<~hw(Ii*(;sig%j`JT zj;Gl1Bs*Sf$LnXUeqq2|%uj)U#ajh6CEJ6>VOZ|DdIRF|Xd*x%Au+Hs_g-raV6gdP9M zj{Pj|>V1noq8r%LS zE6jo3zsEwBdVdHj!h^g=!h^#B-V^Zp5bvq*uyC;VSNMIn_Z&Pw-FrU#O!ygZL3mbp zmbXxR_TCB44ZrLyf-2|jSB|r#`1oKr8a*-?6^sr}!wQVSYJNHx7n~8Ci68iU@C7{3 zxxsnCmxJ;6gNuVn!Q@~{a0xc{YQ7Er_28RW+UtYwd<^;Gi(0`o;CU+J%}(hiU+7n` zf1J(*ABTP2A@OL&JNB&%Dxnw=Y0wfc0RH; zKA7N*g_4uKalxg**S*g`$7$aANb``mH)J4k%<5b00pn~2(+Za@OEF&MNH2$8vSaBf zIRPywpf;yr{baj(W4)L;kM+8a@lfAp?~ncZNA%(*_A0Yohj{O@ALb7t`X1~b;%hyR z!~Mg(Bm5(Y(2w+o`a`{={9}Es_A-K%HIDW#@u#x7$7TLytnWd*;T`8c=s)Zo??2@~ z<(+6+HUf|RE~_9(*UmCsyMXxONN*y1xz_s^>QO<@$-CYjN~J`~JJLUjl8&LA6DZ{} z;4UWBFzP-KJO@+9;XO*n54|6$7Q%@V_}be6TD5?=5$e&?h{Xb~il9jj_Xa_yL#$_R z2&1F=vzpCO+&dafs?5?1hQcSqlhN!$k(kqXCaZHSR_8K$Ca+*0iO2XBsbqmZV;1NP z?+4yL(W@yt^jYsUuig6sbsz5COkF2?Py3hpv%HniY>9um{~r7KP;R$>fsefT(})C* z^uG(&{qWiFc`y#VuYBecfewiw-l{<-&;Yi z=}K>{H_lt{ZJ_*3-d68iug+`mzU=Mrb|Eiqe#)ET=lc2HwSJ*r#9 zxBS7##CJ_5Zt{=wPw=MuC;BIPKlD%XM|wZl075?||D;xajK?8C5kBG~6`~N~bk@9bs zPxpTu7KO$B9r&2u{+&b=1N@)KulRR|Cx$2a|Bi1SaYfRj!1`J>Zf*c~4`a_ki;loD9nGGHG@i@|w0X#70dl#4miZ=cCwdei zb0;HnW!|U#0Z7|tP1?>iX*&;D`MURIq~u%PWdBCmikJBRihNv-eEbF(eFSN^5ovhd z`)4%hO>BWA;OB6zh}g3vDE00O`UD4i_lsNJ@8Q$uy#Izz=Xejmr}Ml&h!fr);lbCv z2j#uIhs6o+PvV64Sa3^li}!eNTX38AXVKkzLKOF&gud^3GelqSnV=@9!4E4!dLBCV z@@9&T-b?16XPY;k18u_a;gDVmdY4{jeS-Hxt>ET+uNeLEphyu`TVt(zVYe&M>4SJb zpgF_?uz{>1g!w==KJr`GFw8c^#gykU*m709ob0BwH3S7;YUGUP#ly6 z6~Q62A&*U+cR!xR>umVxjnY-y^{z zeDe?s#rGT^4<7dpf**`&vN|73i(t0bkG9VoufNrN04<>Ryn(b3KHwXbYl1a=_p&`` z=gX-bK?f1no?s7OoX2K)gK0VBvxZecSitIuWnmd>f0c*jta8;i?CTvxYhs``lorJh zR`N}n0Fi;KifMV>YT%>VCRSDvohla;RW7_#10pFBZwcydneH{ zn&6G3b##$;GA*Qud@uIm@M3Q?Eu~3(_j__U**jG|!QN@Knl9ms#Zy_K=yY07m-2<# z%UGjpEUl=^S(EIF@Ct8Scx8B{_ZeDLS9xd9s=C@cla|#r-dW+b;k9rl@V;R&M3u!5 z1H8-G2U{dDz`N4Bifc*PK$EgVOv(nClpSJHHpu%2?`ufn*VzvR(l@XX-(<&Uvma(M zc(}>n;U0<51=Qc{U5Kvz5MKT>?VuDo_b=R+1?&^t8r;gY z>|h`C@OG~Ml~z&?t)x4-{s}#Hp>&gLwU|PaiCnbw-??)St*1O|J%uJ8y=X)Iiu?Z& z{0Go}9sC;k_|M=!xpQA|A9wyM_%E*S5ANssH^Fbbe6;-c-1$TB2adAB1*S>8V@>*N z@KrcDveU8hJ z#n4)371laS;YHVTU$(vvy<0`K^e!uVT zt{%I3KP}moEX$H5%aUx%l59(|ElZXk@>}>NT37y)%X4X z=bU@ax%X2x@rbq?BjYeK4kHs{kqL3}tFmObiFt7`?+(2- z&Xro`R2J(lf0wr3E&i4IuluiS%RT<6j`JJ-n6?`i%jJEJ%bwkBG+3+jw@Rg zirJRGh%0{)mu)sqC*X8GY%Z4%F4CP6mBkYCO2blF?((o)mfPZ{O3P1O#wNFz*RoE7`Y5VkWy7p|UsFo@^ z^j)3OMg`g!N8Y4@yh#n(XjI;0P#(jyQH3^|q>8`M`O&IiHlB1Y3fYW59gj}7pp)I` zWD7dkjZU_rlWpi^J385hPPU_yE$Cz?I@yIzcA}GA_~mZhY4A|cgZ;x+iXI&i|{r{v^s!R zlc;n6Z?hPE4iZNd5=Rx`d6wXLmg0Gm#8ky-HA!SugBRMQI5{5lquT*=dpW*nfLN;@ z4KGK-mW3{p+AZ!H!aJ=%u`7we%FyjPeAHUyq?_c^OtGub=Mb74!c$#_8dnpujUr~N zz-z6+Yi%HMt3;pch~Gxz!7f9kL#T8k9&CtMZX&T<1F>8qvD_qLxh7({$;5Ka#Bwdf za;?O2Q;6lJ63a~^mTM!Hn@%j(PAu0!EZ0daH-lJiCb8TsV!1BG5OzM`WHAz0fKNU* zh@k5T`gPvUMq}gvqspP~)9cUr&niCmI*+r5!v=Y5P>A)35Y0!4=3`im45Il=EJYSJ z!tSyBMcMvl-z=}ys+F*Fl;36bZ{H@4w)=KP@ExA@wzV#Hp>n=m@|N9N8@pJ!-yTIf zv;Ay&hBHmB*Xs=gH&s`96870Y9Mi*BAH&@^6d%V)?gaewnnVoQJ&D8ox#n z{93;@80Xjf_3~!Qf5?ZZDyInEV(tmdQyueE&pb_Jo*I~^M&@Y}^VGyVO=g~&nWq-! zsg-$}!aPl7o~AKRZOqej=Bb@|>R_HaHKxiD*g3||Gj<_k7h&w8j9rAWi!yeOv5PTw8H`pqGR%whzyHG)k#dduo07{?sOF_&@7V;u7t!2~0i!w42Ic7=>x z5o4Fd*kzFsDrN*r7{O9TFiV;FRnmctV+rF}#yDnc95?FQEq9b)G;Q1sKB|EMX32F$YVSgIUbMQs!V8bFiE_SjHSIXAYJy2P>I_Rm{Ok=3tfXBz-{N zY}o-@;oy*tz%m&tVDt)XOBGQ_I~JxI3o{O;FAeU}qlP(K$eb;L`-_>mE@o~B?ssF6 zDww$*?2=>tW@DW^vp8RMs5Y_Hswo3#VlkE~!n`hKUVBufnknX*KIUM%O0iurY*z+~ zk>!%@%4F^rV8ODmVA)u(GT2}l-1$KlZ0N#{#bHA)Y?ucdx?sah*wBk*OJLb@U`8MI zEf@QihkeV3D?@0m8j>mr1Vn21SbLA}Ru%GqV&xzR22JB}e_Hz>UvkCh-8T;9c{cOR0wqif0U_Yl~ zKc`_oJFuT^*w5)=s?~3vk^0})!&5y9Rm-UgBC6+92T^KBF*3Lrs_oS4&dgRx%2JJ| zQFo5FdQw~&$d({MRVjyhQZDtRJY^Q2)&9NM7tUEvI#qxCdsINd3u;HMPV=7X&jP8`>RpQW{vCYGl z!jLi;qP0iFkWny1tAZ%2RS82z!;mpBWGoD+f+5u~WE>2sfg$5zNG%MR07L3vNIeXh z2tyh)3d8z;Bg$m!UQC)3>n-=1vrN5C)e5Z3+g7BH>s>lO)ONe#D8f8PndcZYTunaQ z?(8O;7*`Zng|G(R<*P#Yghu8^!OI%GS5zT!%H5yW>vz?? z=~X>^Ut2zK(t#^*x`NA!^RH|QeG;ED3zpwnTa{QCE#n@V{I-Sb zXIHu*y}rz?3rgI2u_uH*)vC~K3QARVxmrCFpL5r01&7bO8?>UtVRuApO5Et~k^=5_ zN2TSzRoya6RhQF2xjL?X9E_vxmaY2QJKFxP`@Pn_Fnz>TjVV|BXDUgck{n;_>olHT zo<7A7Q@V#Gx;va6R+EkEZ^n$&)o4-dKnDECO!LF+Ko%^?h9w!WBrZObrnzG4;FfD2 z#+4knk_%THT*-qg`EaEGt`x$RBGp*5PK)X+wzh>;flJ{{8N4Z%_9kh6O?p(op-MPZ z8BEnnoElipSlNw;s zM3~e-)YMAU)C!YYVA2FJ>4#$f6MAG{z@!LF3dN-N^#7OzkBd)vVp6_X6^B((SQWzt zX27kOxK*dQS2uvT)g*3ZiCfLGfo8`WRWWJRYwHon7TY?-m>I5Ldz0&g~PA_q#)?z*wcv!M{BCmk<96#J|6iox4*kEP;hZbRQI} z4)~z9SdUModL4ft1{*KS;AIuOELY9ynP8OFbhH+V>g+=6P^eV(>No0Qu%4hwRk7aD ze$Kh`(uOg33=AGC2IuN*CeIfK&D0aCU~{S)WE^a+5#z2GZzF*sbv?|+$AVrs-GSw6 zhuIyPi)``4_-$)SCG<|Se4X&S7kIiyjy7_|p$+Jmo*;vdK0A7kjD1|iw63!uk)PQ*IJ^1UaV|CR(38{wjV3okCp9!>-}(jHdb~jR(3ADpMaI^g5`Cv zydIWM!^%#C-`!Z*ZrI!goBLq%46JN79GwYA8?dr{@UjsrI|*Jk5d&xyt8i72Neqxh z44@UF!qv*rS`3gN2GE*H;X19PVU?{sVt{;NfC6HGLSld-Vt`^|fD&SWQeuEIVt{gD zfKkK%R^8UMvhJ45BSEBZ{@_qhvGsE!dxF88Bila{oZPed&_4A5MD!lJr;lAnXLnf8 z%3xyUWLeKy(#}HlK&{bP=63Pmp!zNUEclYN<9Y{HatCid=cFy&kzYKoRTX@?`dsGe zTEi8=TAdZ`5OWU6B3`fgw{Nis!eYoW>0yGn-Xbn{2Yta}t(mh<+_VhrmGZeMlW*S${Anp0C%rmi2^QrI_rndVA!Nmn<8s4tiEDTVnOM)c<9ktWfH# zmd>U~FSFG}xkUOHR&8&$*!(Ao<-Qo)ls-Q5ctz5&-Jxlcti+69j;^3wDkWJb>J{Ss z)xnFCQ_hhS)ew(D#nd&)Opq`%9(%Uv^Xhd50LU3bnOYj$)H}5$VZUDD| zd%#2Bwcug!rmb7|9tdv*?*Na2C%}{7De&=a`}Xe*&w$T@FM_XtuYqrZZ|@k{yfr)z zzHb}NHf?5&IIRx^T9!I<(_?4?v1PkH-g*1E5HNbRp7OI_HW%2 zxgPuyc#A$d61f$;9lRSn2A%-F3qH7CPmzbg$H9LDp8;P0Ujkp;)HqOTi&;e10Mc{I9HMrs6m0J$Rwt&09ec&PR8t@C?k%Lzr zI2gML{7djQ@J{e3cpSX{n(ZU|W8Vi)fscVtf=`3bgLWl~!d&d#r9WGbkA42qp6x8k zzVXx1D+;kr$4h;7vXb@<7lT~KBkOJN|70|)YUjv?A((m5n8Q0S2CXA4byfGLVJYvt z7_>V#SdY*rqPs@#$yWqxsAyKW3)}*309S+7lT+Z9f(yW2P*ntNZwKuvg#yCy4&Zcpj9K{ha=0cP#z;chb)%($6*@_PKARpN|o{IA4=~ZsLeCRWZF^JqFLKp7=Xe=8Z%8 zRG8jNcs%go(>gm_r@pt}yASl+qZj<f#m$P}bkkzSW#wM+W8 z5BIGe;ner0t~t6+_4?~on?Isz{LQMf->N$L?W&gFsml0KRl!fF)_qdd?MGFa){&}* z$%TD760;W(ofTQr3?CAn2>(v_?eG-u5IfZ8;?&2IVu#&( zGqvUE3%2NQ>hN;9n#LGp@3Ae9>c_0oM_W#6i&>@A7N<@`7jmufhg)>jjLpdf-xbQj zs(0nXEk3+mGjoYN_&=PP+cY0GW>!VEwIaH7j^cvbwatfzW!ZDAgICv>zYsnU{zTX5 z|8w}$@Mqym;mf-I{paB?!e553hQHD$BI@SdfpFheHwO! z+E>(P@ilK(sr|LOChk>l!~^Pvcto8HujzAM^JiyGq0Yzby7L~Ly*SRVv-Rwxc4PR; za9Dq{R~y3Zyz>9!{n8e9f(fw0_Bs3efH0>0TTEctPDf3zoZ8wb`dKRIp{UgRqvWLu zU6C@L`n($!&ekX6?B_?plfhH+U8iNoe;hom4)bT!Vg79JoUHmk>FS4{1TX3eh@S>O zlZ}5_=l1N!4eR?h>U%DIWYpQg=$PkH91|YbeHe^|cK2{Pi0v4q68lfNr;M)LF#Gb& z@Ovs3SO>WDrbi0%^{QKEd^I}ry`(7>Z`lv^nBm1tDnmRJ(T-7jL z|F3$~Ue#XJjjh_g)hNf>k*c32bv@&~7j#{JFWqlG)fhpR=sTh^D|t}YO}UU9DOxX>TYSJ#JK7pSf*b!0hd zAtJ8)|ID_!+r&2GtmW?Pj?tRA@IK9R7~1$n!Ur|aQRdfTfRsJ4XS-8@*U9P5^y~bC gsn?zQJ%66EB2CqU%7-*n4=6YChrjDRX3c{C0<1)TrvLx| literal 0 HcmV?d00001 diff --git a/croak/gui/assets/fonts/LICENSE.txt b/croak/gui/assets/fonts/LICENSE.txt new file mode 100644 index 0000000..01497cc --- /dev/null +++ b/croak/gui/assets/fonts/LICENSE.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/croak/gui/plots.py b/croak/gui/plots.py new file mode 100644 index 0000000..1be0de4 --- /dev/null +++ b/croak/gui/plots.py @@ -0,0 +1,566 @@ +"""On-screen plots: white pyqtgraph views with a crosshair and a hover readout. + +A view draws the arrays it is given and nothing else. What to show is decided +by the data functions in :mod:`croak.plotting`, which the exported matplotlib +figures share, so the screen and a saved figure agree. Mouse zoom and pan are on +(they replace the matplotlib toolbars); a double-click returns to the view the +data was shown with. + +The readout under the pointer comes from a ``reader(x, y) -> str`` the owner +sets; :func:`map_reader` and :func:`curve_reader` build the usual ones. +""" + +from __future__ import annotations + +from collections.abc import Callable, Sequence +from dataclasses import dataclass, replace +from functools import partial +from typing import cast + +import numpy as np +import pyqtgraph as pg +from PySide6.QtCore import QPointF, QRectF, Qt +from PySide6.QtGui import QFont, QMouseEvent, QPainter + +from .theme import FONT_FAMILY, PALETTE, PLOT_PX + +__all__ = [ + "COLOURS", + "Curve", + "LinePlot", + "MapPlot", + "Plot", + "curve_reader", + "map_reader", + "signed_colormap", +] + +pg.setConfigOptions(antialias=True, background="w", foreground=PALETTE.ink) + +COLOURS = ( + "#1f6fb4", # blue + "#d2670b", # orange + "#2a9d6a", # green + "#8c5bb5", # purple + "#c2445c", # red + "#5a6675", # grey + "#b08a16", # ochre + "#2b8fb3", # cyan +) +"""The curves' colours, in order of use.""" + +PEN_WIDTH = 1.7 +"""A curve's line width, in logical pixels.""" + +NEGATIVE_RGBA = (0.70, 0.15, 0.12, 1.0) +"""The colour of the most negative value on a signed map (fading to white at 0).""" + +_CROSSHAIR = (120, 130, 140, 190) +_NOTE_FILL = (27, 36, 48, 235) +_LABEL_STYLE = { + "color": PALETTE.muted, + "font-size": f"{PLOT_PX}px", + "font-family": FONT_FAMILY, +} +# Past these fractions of the view the readout flips to the other side of the +# pointer, so it never runs off the plot. +_FLIP_X = 0.62 +_FLIP_Y = 0.9 + + +def _font(px: int = PLOT_PX) -> QFont: + font = QFont(FONT_FAMILY) + font.setPixelSize(px) + return font + + +def _set_label(axis: pg.AxisItem, text: str) -> None: + """Label ``axis`` in the plots' label style.""" + # setLabel takes CSS properties as **kwargs; its stubs name only a few. + axis.setLabel(text, **_LABEL_STYLE) # pyright: ignore[reportArgumentType] + + +def _style_axis(axis: pg.AxisItem, label: str) -> None: + """Give ``axis`` the plots' look: grey, inward ticks, no SI rescaling.""" + axis.setPen(pg.mkPen(PALETTE.axis)) + axis.setTextPen(pg.mkPen(PALETTE.muted)) + axis.setTickFont(_font()) + axis.setStyle(tickLength=-4, tickTextOffset=3) + # The label states its unit; a silent "(×1e-3)" would contradict it. + axis.enableAutoSIPrefix(False) + _set_label(axis, label) + + +class _SmoothImage(pg.ImageItem): + """An image drawn with bilinear smoothing between its samples.""" + + def paint(self, painter: QPainter, *args: object) -> None: + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True) + super().paint(painter, *args) + + +class Plot(pg.PlotWidget): + """A plot box with a crosshair and a hover readout. + + Parameters + ---------- + xlabel, ylabel : str + The axis labels, with their units. + """ + + def __init__(self, *, xlabel: str = "", ylabel: str = "") -> None: + super().__init__(background="w") + self.setMinimumSize(140, 100) + self.reader: Callable[[float, float], str] | None = None + self._home: tuple[tuple[float, float], tuple[float, float]] | None = None + # A PlotWidget always owns its PlotItem; the stubs allow None. + self.item = cast(pg.PlotItem, self.getPlotItem()) + self.item.hideButtons() + self.item.setMenuEnabled(False) + self.item.layout.setContentsMargins(0, 8, 8, 0) # pyright: ignore[reportAttributeAccessIssue] + self.box = self.item.getViewBox() + _style_axis(self.item.getAxis("bottom"), xlabel) + _style_axis(self.item.getAxis("left"), ylabel) + self.item.getAxis("left").setWidth(54) + self.item.getAxis("bottom").setHeight(38) + # A closed box: top and right axes drawn, without numbers. + for side in ("top", "right"): + self.item.showAxis(side) + axis = self.item.getAxis(side) + axis.setPen(pg.mkPen(PALETTE.axis)) + axis.setStyle(showValues=False, tickLength=0) + pen = pg.mkPen(_CROSSHAIR) + self.cross_x = pg.InfiniteLine(angle=90, pen=pen) + self.cross_y = pg.InfiniteLine(angle=0, pen=pen) + self.note = pg.TextItem("", color="#fff", fill=_NOTE_FILL, anchor=(0, 1)) + self.note.setFont(_font(PLOT_PX + 1)) + for extra in (self.cross_x, self.cross_y, self.note): + self.addItem(extra, ignoreBounds=True) # pyright: ignore[reportCallIssue] + extra.setZValue(50) + extra.hide() + self.scene().sigMouseMoved.connect(self.hover) # pyright: ignore[reportOptionalMemberAccess, reportAttributeAccessIssue] + + # -- labels and view ---------------------------------------------------- + def set_labels(self, xlabel: str | None = None, ylabel: str | None = None) -> None: + """Change the axis labels (``None`` keeps a label).""" + if xlabel is not None: + _set_label(self.item.getAxis("bottom"), xlabel) + if ylabel is not None: + _set_label(self.item.getAxis("left"), ylabel) + + def set_view( + self, xrange: tuple[float, float], yrange: tuple[float, float] + ) -> None: + """Show ``xrange`` × ``yrange``, and return there on a double-click.""" + self._home = (xrange, yrange) + self.box.setRange(xRange=xrange, yRange=yrange, padding=0) + + def reset_view(self) -> None: + """Return to the view last set with :meth:`set_view` (else fit the data).""" + if self._home is None: + self.box.autoRange(padding=0.02) + else: + self.box.setRange(xRange=self._home[0], yRange=self._home[1], padding=0) + + def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: # noqa: N802 — Qt override + self.reset_view() + event.accept() + + # -- hover -------------------------------------------------------------- + def hover(self, position: QPointF) -> None: + """Show the crosshair and the readout at scene point ``position``.""" + inside = self.box.sceneBoundingRect().contains(position) + if not inside or self.reader is None: + self.clear_hover() + return + point = self.box.mapSceneToView(position) + text = self.reader(point.x(), point.y()) + if not text: + self.clear_hover() + return + self.cross_x.setPos(point.x()) + self.cross_y.setPos(point.y()) + self.note.setText(f" {text} ") + (x0, x1), (y0, y1) = self.box.viewRange() + right = (point.x() - x0) / ((x1 - x0) or 1.0) > _FLIP_X + top = (point.y() - y0) / ((y1 - y0) or 1.0) > _FLIP_Y + self.note.setAnchor((1.0 if right else 0.0, 0.0 if top else 1.0)) + self.note.setPos(point.x(), point.y()) + for extra in (self.cross_x, self.cross_y, self.note): + extra.show() + + def clear_hover(self) -> None: + """Hide the crosshair and the readout.""" + for extra in (self.cross_x, self.cross_y, self.note): + extra.hide() + + def leaveEvent(self, event: object) -> None: # noqa: N802 — Qt override # pyright: ignore[reportIncompatibleMethodOverride] + self.clear_hover() + super().leaveEvent(event) # pyright: ignore[reportArgumentType] + + +def signed_colormap(name: str, low: float, high: float) -> pg.ColorMap: + """Return colour map ``name`` for levels ``(low, high)``, white at zero. + + A measured trace can dip below zero after background subtraction. One map + shows both signs: the negative part fades from red to white and the positive + part runs white → ``name`` (whose own lowest colour is replaced by white, + so a zero background reads as paper). ``low >= 0`` gives the plain + white-based map. + + Parameters + ---------- + name : str + A pyqtgraph/matplotlib colour map name, e.g. ``"viridis"``. + low, high : float + The levels the map will span. + + Returns + ------- + pyqtgraph.ColorMap + Stops at the positions of ``low``, ``0`` and ``high``. + + Raises + ------ + ValueError + If ``high <= 0``, ``low >= high`` or ``name`` is not a colour map. + """ + if not high > 0 or not low < high: + raise ValueError(f"need low < high and high > 0, got ({low}, {high})") + try: + base = pg.colormap.get(name) + except FileNotFoundError: # pyqtgraph looks the name up as a file + base = None + if base is None: + raise ValueError(f"unknown colour map {name!r}") + stops = np.linspace(0.0, 1.0, 9) + colours = np.array(base.map(stops, mode=pg.ColorMap.FLOAT), dtype=float) + colours[0] = (1.0, 1.0, 1.0, 1.0) + if low >= 0: + # pg.ColorMap reads numeric colours on a 0–255 scale. + return pg.ColorMap(stops, 255 * colours) + zero = -low / (high - low) + positions = np.concatenate([[0.0], zero + (1.0 - zero) * stops]) + return pg.ColorMap(positions, 255 * np.vstack([NEGATIVE_RGBA, colours])) + + +def is_uniform(axis: np.ndarray) -> bool: + """Whether ``axis`` increases in equal steps (to 0.1 %). + + Examples + -------- + >>> is_uniform(np.array([0.0, 1.0, 2.0])) + True + >>> is_uniform(np.array([0.0, 1.0, 3.0])) + False + """ + steps = np.diff(np.asarray(axis, float)) + if steps.size == 0 or not np.all(steps > 0): + return False + return bool(np.allclose(steps, steps[0], rtol=1e-3, atol=0)) + + +class MapPlot(Plot): + """A false-colour image with a colour bar. + + Parameters + ---------- + xlabel, ylabel : str + The axis labels. + colours : str + The colour map, e.g. ``"viridis"`` or ``"bwr"``. + unit : str + The colour bar's label. + signed : bool + Draw values below zero in red fading to white (:func:`signed_colormap`). + """ + + def __init__( + self, + *, + xlabel: str = "", + ylabel: str = "", + colours: str = "viridis", + unit: str = "", + signed: bool = False, + ) -> None: + super().__init__(xlabel=xlabel, ylabel=ylabel) + self.colours = colours + self.signed = signed + self.data: np.ndarray | None = None + self.x_axis = np.zeros(0) + self.y_axis = np.zeros(0) + self.image = _SmoothImage(axisOrder="row-major") + self.addItem(self.image) + self.bar = pg.ColorBarItem( + colorMap=pg.colormap.get(colours), + interactive=False, + width=10, + colorMapMenu=False, + ) + self.bar.setImageItem(self.image, insert_in=self.item) + _style_axis(self.bar.axis, unit) + self.message = pg.TextItem("", color=PALETTE.muted, anchor=(0.5, 0.5)) + self.addItem(self.message, ignoreBounds=True) # pyright: ignore[reportCallIssue] + + def show_map( + self, + data: np.ndarray, + x: np.ndarray, + y: np.ndarray, + levels: tuple[float, float], + ) -> None: + """Show ``data`` (rows along ``y``, columns along ``x``) spanning ``levels``. + + Parameters + ---------- + data : numpy.ndarray, shape (len(y), len(x)) + The image; NaN draws as blank. + x, y : numpy.ndarray + The sample positions, increasing and evenly spaced (the data + functions resample a non-uniform axis before it reaches the screen). + levels : tuple of float + The colour bar's span. + + Raises + ------ + ValueError + If the shapes disagree or an axis is not evenly spaced. + """ + x = np.asarray(x, float) + y = np.asarray(y, float) + data = np.asarray(data, float) + if data.shape != (y.size, x.size): + raise ValueError(f"data {data.shape} does not match y×x {(y.size, x.size)}") + if not (is_uniform(x) and is_uniform(y)): + raise ValueError("a map's axes must be increasing and evenly spaced") + self.data, self.x_axis, self.y_axis = data, x, y + self.message.hide() + self.image.setImage(data, autoLevels=False) + dx, dy = x[1] - x[0], y[1] - y[0] + # Each sample is a pixel centred on its position. + self.image.setRect( + QRectF(x[0] - dx / 2, y[0] - dy / 2, dx * x.size, dy * y.size) + ) + self.set_levels(levels) + self.set_view((x[0] - dx / 2, x[-1] + dx / 2), (y[0] - dy / 2, y[-1] + dy / 2)) + + def update_map(self, data: np.ndarray) -> None: + """Replace the image's values in place, on the same axes and levels.""" + data = np.asarray(data, float) + if self.data is None or data.shape != self.data.shape: + raise ValueError("update_map needs an image of the shape already shown") + self.data = data + self.image.setImage(data, autoLevels=False) + + def set_levels(self, levels: tuple[float, float]) -> None: + """Change the colour bar's span without touching the data.""" + low, high = float(levels[0]), float(levels[1]) + if self.signed and high > 0 and low < high: + self.bar.setColorMap(signed_colormap(self.colours, low, high)) + self.bar.setLevels(values=(low, high)) + + def value_at(self, x: float, y: float) -> float | None: + """The sample nearest ``(x, y)``, or ``None`` outside the image.""" + if self.data is None: + return None + xs, ys = self.x_axis, self.y_axis + col = int(round((x - xs[0]) / (xs[1] - xs[0]))) + row = int(round((y - ys[0]) / (ys[1] - ys[0]))) + if not (0 <= col < xs.size and 0 <= row < ys.size): + return None + return float(self.data[row, col]) + + def say(self, text: str) -> None: + """Show ``text`` in place of the image.""" + self.image.clear() + self.data = None + self.message.setText(text) + (x0, x1), (y0, y1) = self.box.viewRange() + self.message.setPos((x0 + x1) / 2, (y0 + y1) / 2) + self.message.show() + + +@dataclass(frozen=True) +class Curve: + """One line of a :class:`LinePlot`. + + Attributes + ---------- + x, y : numpy.ndarray + The points. + name : str + The legend entry ("" for none). + colour : str + Its colour (default: the next of :data:`COLOURS`). + dashed : bool + Draw dashed. + right : bool + Plot against the right-hand axis (e.g. a phase beside an intensity). + """ + + x: np.ndarray + y: np.ndarray + name: str = "" + colour: str = "" + dashed: bool = False + right: bool = False + + +class LinePlot(Plot): + """Curves against a shared x axis, with an optional right-hand y axis. + + Parameters + ---------- + xlabel, ylabel : str + The axis labels. + right_label : str or None + The right-hand axis's label; ``None`` for no right-hand axis. + """ + + def __init__( + self, *, xlabel: str = "", ylabel: str = "", right_label: str | None = None + ) -> None: + super().__init__(xlabel=xlabel, ylabel=ylabel) + self.curves: list[Curve] = [] + self.items: list[pg.PlotDataItem] = [] + self.log_y = False + self.legend: pg.LegendItem | None = None + self.right_box: pg.ViewBox | None = None + self.marks: list[pg.InfiniteLine] = [] + if right_label is not None: + right = pg.ViewBox() + self.right_box = right + self.item.scene().addItem(right) # pyright: ignore[reportOptionalMemberAccess] + axis = self.item.getAxis("right") + axis.linkToView(right) + _style_axis(axis, right_label) + axis.setStyle(showValues=True) + right.setXLink(self.box) + match = partial(self._match_right, right) + self.box.sigResized.connect(match) + match() + + def _match_right(self, right: pg.ViewBox, _resized: object = None) -> None: + """Keep the right-hand view box over the main one as it resizes. + + ``_resized`` is the view box ``sigResized`` passes along; unused. + """ + right.setGeometry(self.box.sceneBoundingRect()) + right.linkedViewChanged(self.box, right.XAxis) + + def _points(self, curve: Curve, x: np.ndarray, y: np.ndarray): + """The points of ``curve`` to draw: on a log axis, only ``y > 0``.""" + x, y = np.asarray(x, float), np.asarray(y, float) + if self.log_y and not curve.right: + keep = y > 0 + return x[keep], y[keep] + return x, y + + def set_curves(self, curves: Sequence[Curve], *, log_y: bool = False) -> None: + """Replace the curves; a legend appears when more than one is named. + + On a log axis values ``<= 0`` are dropped rather than drawn as -inf. + """ + for item, curve in zip(self.items, self.curves, strict=True): + box = self.right_box if curve.right else None + (box or self.item).removeItem(item) + if self.legend is not None: + self.legend.clear() + self.items = [] + self.curves = list(curves) + self.log_y = log_y + self.item.setLogMode(False, log_y) + named = [c for c in curves if c.name] + if len(named) > 1 and self.legend is None: + self.legend = self.item.addLegend(offset=(-8, 8), labelTextSize="8pt") + if self.legend is not None: + self.legend.setVisible(len(named) > 1) + auto = 0 + for curve in self.curves: + colour = curve.colour or COLOURS[auto % len(COLOURS)] + auto += not curve.colour + style = Qt.PenStyle.DashLine if curve.dashed else Qt.PenStyle.SolidLine + pen = pg.mkPen(colour, width=PEN_WIDTH, style=style) + x, y = self._points(curve, curve.x, curve.y) + item = pg.PlotDataItem(x, y, pen=pen) + if curve.right and self.right_box is not None: + self.right_box.addItem(item) + else: + self.item.addItem(item) + if self.legend is not None and curve.name: + self.legend.addItem(item, curve.name) + self.items.append(item) + + def update_curve(self, index: int, x: np.ndarray, y: np.ndarray) -> None: + """Move curve ``index``'s points in place (cheap: no legend or layout).""" + curve = replace(self.curves[index], x=np.asarray(x), y=np.asarray(y)) + self.curves[index] = curve + self.items[index].setData(*self._points(curve, curve.x, curve.y)) + + def set_right_range(self, low: float, high: float) -> None: + """Fix the right-hand axis's range.""" + if self.right_box is not None: + self.right_box.setYRange(low, high, padding=0) + + def set_marks(self, positions: Sequence[float], colour: str = "") -> None: + """Draw dashed vertical lines at ``positions`` (replacing the last set).""" + for mark in self.marks: + self.item.removeItem(mark) + pen = pg.mkPen(colour or PALETTE.accent, width=1.2, style=Qt.PenStyle.DashLine) + self.marks = [] + for position in positions: + line = pg.InfiniteLine(pos=position, angle=90, pen=pen) + self.addItem(line, ignoreBounds=True) # pyright: ignore[reportCallIssue] + self.marks.append(line) + + def fit(self) -> None: + """Fit the view to the left-axis curves, and make that the home view.""" + drawn = [ + self._points(c, c.x, c.y) + for c in self.curves + if not c.right and np.size(c.x) + ] + if not drawn: + return + x = np.concatenate([d[0] for d in drawn]) + y = np.concatenate([d[1] for d in drawn]) + y = y[np.isfinite(y)] + if self.log_y: + y = np.log10(y) + yr = (float(np.min(y)), float(np.max(y))) if y.size else (0.0, 1.0) + pad = 0.05 * ((yr[1] - yr[0]) or 1.0) + self.set_view((float(np.min(x)), float(np.max(x))), (yr[0] - pad, yr[1] + pad)) + + +def map_reader( + plot: MapPlot, xname: str, yname: str, value: str = "{:.3g}" +) -> Callable[[float, float], str]: + """Return a reader that says the position and the map's value there.""" + + def read(x: float, y: float) -> str: + found = plot.value_at(x, y) + if found is None: + return "" + return f"{xname} {x:.4g} {yname} {y:.4g} {value.format(found)}" + + return read + + +def curve_reader( + plot: LinePlot, xname: str, value: str = "{:.3g}" +) -> Callable[[float, float], str]: + """Return a reader that says x and each left-axis curve's value there.""" + + def read(x: float, _y: float) -> str: + parts = [f"{xname} {x:.4g}"] + for curve in plot.curves: + cx = np.asarray(curve.x, float) + if curve.right or cx.size == 0 or not (cx[0] <= x <= cx[-1]): + continue + found = float(np.interp(x, cx, np.asarray(curve.y, float))) + name = f"{curve.name} " if curve.name else "" + parts.append(f"{name}{value.format(found)}") + return " ".join(parts) if len(parts) > 1 else "" + + return read diff --git a/croak/gui/sheet.py b/croak/gui/sheet.py new file mode 100644 index 0000000..6970a87 --- /dev/null +++ b/croak/gui/sheet.py @@ -0,0 +1,501 @@ +"""The building blocks of a stage: a form of folding sections beside its plots. + +A :class:`Sheet` is the layout every stage shares: a :class:`Form` on the left +(sections of rows, the one-line help for the focused row, and a +:class:`ReadyLine`) and the stage's plots on the right, which move below the +form when the window is narrow. + +Help text comes from the stage's ``HELP`` sections (:mod:`croak.gui.help`): +a row added to section ``S`` with label ``L`` takes ``HELP[(S, L)]`` as its +tooltip and as the help line shown while it has focus; a row whose editor is a +check box is keyed by the box's text. :attr:`Form.matched` records the keys that +found a row, so a test can check that no help entry is orphaned. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import cast + +from PySide6.QtCore import QEvent, QObject, Qt, Signal +from PySide6.QtWidgets import ( + QApplication, + QBoxLayout, + QCheckBox, + QFrame, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QProgressBar, + QPushButton, + QScrollArea, + QSizePolicy, + QToolButton, + QVBoxLayout, + QWidget, +) + +from . import settings +from .help import HelpSections, flatten +from .theme import label, set_flag + +__all__ = [ + "AdviceBox", + "DerivedGrid", + "Form", + "ProgressStrip", + "ReadyLine", + "Row", + "Section", + "Segments", + "Sheet", +] + +LABEL_WIDTH = 124 +"""The width of a row's label column, logical px.""" + +NARROW_WIDTH = 1000 +"""Below this window width (logical px) a sheet's plots move under its form.""" + +FORM_MIN_WIDTH = 360 +"""The narrowest the form column may be, logical px.""" + + +def plain(text: str) -> str: + """Undo Qt's ``&&`` escape, giving a label as the user reads it. + + Examples + -------- + >>> plain("Skip filtering && regrid") + 'Skip filtering & regrid' + """ + return text.replace("&&", "&") + + +class Row(QWidget): + """One setting: a label in the label column beside its editor. + + A check box carries its own text and takes the whole width. + """ + + def __init__(self, text: str, editor: QWidget, help_text: str = "") -> None: + super().__init__() + self.editor = editor + self.help_text = help_text + self.text = plain(editor.text()) if isinstance(editor, QCheckBox) else text + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 2, 0, 2) + layout.setSpacing(8) + self.label: QLabel | None = None + if not isinstance(editor, QCheckBox): + self.label = label(text, "rowLabel") + self.label.setFixedWidth(LABEL_WIDTH) + self.label.setWordWrap(True) + layout.addWidget(self.label) + layout.addWidget(editor, stretch=1) + if help_text: + editor.setToolTip(help_text) + if self.label is not None: + self.label.setToolTip(help_text) + + def changeEvent(self, event: QEvent) -> None: # noqa: N802 — Qt override + # The label greys with its editor: a disabled row reads as unavailable. + if event.type() == QEvent.Type.EnabledChange and self.label is not None: + self.label.setEnabled(self.isEnabled()) + super().changeEvent(event) + + def matches(self, query: str) -> bool: + """Whether ``query`` (case-insensitive) is in this row's label or help.""" + query = query.casefold() + return query in self.text.casefold() or query in self.help_text.casefold() + + +class Section(QWidget): + """A titled group of rows that folds open and shut in place. + + Parameters + ---------- + title : str + Its heading, and the first half of its rows' help keys. + help_map : mapping + ``(section, label) -> text``, from the stage's ``HELP``. + matched : set + Where the help keys this section uses are recorded. + folded : bool + Its initial state; a remembered state (``settings_key``) wins. + settings_key : str + Remember the fold state under this key ("" to not remember it). + """ + + toggled = Signal(bool) + + def __init__( + self, + title: str, + help_map: Mapping[tuple[str, str], str], + matched: set[tuple[str, str]], + *, + folded: bool = False, + settings_key: str = "", + ) -> None: + super().__init__() + self.title = title + self.rows: list[Row] = [] + self._help = help_map + self._matched = matched + self._key = settings_key + if settings_key: + folded = not settings.value(settings_key, not folded) + outer = QVBoxLayout(self) + outer.setContentsMargins(0, 0, 0, 6) + outer.setSpacing(0) + self.header = QToolButton() + self.header.setObjectName("sectionHeader") + self.header.setText(title) + self.header.setCheckable(True) + self.header.setChecked(not folded) + self.header.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.header.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.header.toggled.connect(self._fold) + outer.addWidget(self.header) + self.body = QWidget() + self.layout_ = QVBoxLayout(self.body) + self.layout_.setContentsMargins(14, 0, 0, 2) + self.layout_.setSpacing(2) + outer.addWidget(self.body) + self._fold(not folded, remember=False) + + @property + def folded(self) -> bool: + """Whether the body is hidden.""" + return not self.header.isChecked() + + def set_folded(self, folded: bool) -> None: + """Fold or unfold (remembered, when the section has a key).""" + self.header.setChecked(not folded) + + def _fold(self, open_: bool, remember: bool = True) -> None: + self.header.setArrowType( + Qt.ArrowType.DownArrow if open_ else Qt.ArrowType.RightArrow + ) + self.body.setVisible(open_) + if remember and self._key: + settings.set_value(self._key, open_) + self.toggled.emit(open_) + + def add_row(self, text: str, editor: QWidget) -> Row: + """Add a setting labelled ``text`` (ignored for a check box) and return it.""" + key_label = plain(editor.text()) if isinstance(editor, QCheckBox) else text + key = (self.title, key_label) + help_text = self._help.get(key, "") + if help_text: + self._matched.add(key) + row = Row(text, editor, help_text) + self.rows.append(row) + self.layout_.addWidget(row) + return row + + def add_widget(self, widget: QWidget) -> None: + """Add a widget across the section's width (a button row, a table, …).""" + self.layout_.addWidget(widget) + + +class Form(QFrame): + """The left column of a sheet: sections in a scroll area, help, ready line. + + Parameters + ---------- + help_sections : HelpSections + The stage's ``HELP``. + find : bool + Offer "Find a setting", which shows only the rows that match. + """ + + def __init__( + self, help_sections: HelpSections | None = None, *, find: bool = False + ) -> None: + super().__init__() + self.setObjectName("form") + self.setMinimumWidth(FORM_MIN_WIDTH) + self.help_map = flatten(help_sections or []) + self.matched: set[tuple[str, str]] = set() + self.sections: list[Section] = [] + outer = QVBoxLayout(self) + outer.setContentsMargins(0, 0, 0, 0) + outer.setSpacing(0) + self.find: QLineEdit | None = None + if find: + self.find = QLineEdit() + self.find.setObjectName("find") + self.find.setPlaceholderText("Find a setting") + self.find.setClearButtonEnabled(True) + self.find.textChanged.connect(self.filter) + box = QWidget() + bl = QVBoxLayout(box) + bl.setContentsMargins(18, 12, 14, 0) + bl.addWidget(self.find) + outer.addWidget(box) + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + content = QWidget() + self.content = QVBoxLayout(content) + self.content.setContentsMargins(18, 10, 14, 10) + self.content.setSpacing(4) + self.content.addStretch(1) + scroll.setWidget(content) + outer.addWidget(scroll, stretch=1) + foot = QWidget() + fl = QVBoxLayout(foot) + fl.setContentsMargins(18, 8, 14, 12) + fl.setSpacing(6) + self.help = label("", "help", wrap=True) + self.help.setMinimumHeight(32) + self.help.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + self.ready = ReadyLine() + fl.addWidget(self.help) + fl.addWidget(self.ready) + outer.addWidget(foot) + # A widget exists only under a QApplication; the stubs allow None. + cast(QApplication, QApplication.instance()).focusChanged.connect( + self._focus_moved + ) + + def add_section( + self, title: str, *, folded: bool = False, settings_key: str = "" + ) -> Section: + """Append a section and return it.""" + section = Section( + title, + self.help_map, + self.matched, + folded=folded, + settings_key=settings_key, + ) + self.sections.append(section) + self.content.insertWidget(self.content.count() - 1, section) + return section + + def add_widget(self, widget: QWidget) -> None: + """Append a widget outside any section (a heading, a file picker, …).""" + self.content.insertWidget(self.content.count() - 1, widget) + + def _focus_moved(self, _old: QWidget | None, new: QWidget | None) -> None: + widget: QObject | None = new + while widget is not None and not isinstance(widget, Row): + widget = widget.parent() + if isinstance(widget, Row) and self.isAncestorOf(widget): + self.help.setText(widget.help_text) + + def filter(self, query: str) -> None: + """Show only the rows whose label or help contains ``query``.""" + query = query.strip() + for section in self.sections: + hits = 0 + for row in section.rows: + hit = not query or row.matches(query) + row.setVisible(hit) + hits += hit + section.setVisible(not query or hits > 0) + if query and hits: + section.body.setVisible(True) + elif not query: + section.body.setVisible(not section.folded) + + +class Sheet(QWidget): + """A form beside the stage's plots; the plots move below on a narrow window.""" + + def __init__(self, form: Form, plots: QWidget) -> None: + super().__init__() + self.form = form + self.plots = plots + self.split = QBoxLayout(QBoxLayout.Direction.LeftToRight, self) + self.split.setContentsMargins(0, 0, 0, 0) + self.split.setSpacing(0) + self.split.addWidget(form, stretch=2) + self.split.addWidget(plots, stretch=3) + + @property + def narrow(self) -> bool: + """Whether the plots are below the form.""" + return self.split.direction() == QBoxLayout.Direction.TopToBottom + + def resizeEvent(self, event) -> None: # noqa: N802, ANN001 — Qt override + narrow = self.width() < NARROW_WIDTH + if narrow != self.narrow: + self.split.setDirection( + QBoxLayout.Direction.TopToBottom + if narrow + else QBoxLayout.Direction.LeftToRight + ) + super().resizeEvent(event) + + +class ReadyLine(QLabel): + """Whether the stage can go on: green, amber with a warning, red when not.""" + + def __init__(self) -> None: + super().__init__() + self.setObjectName("ready") + self.setWordWrap(True) + self.state = "ok" + + def say(self, text: str, state: str = "ok") -> None: + """Show ``text`` as ``state``: ``"ok"``, ``"warn"`` or ``"error"``. + + Raises + ------ + ValueError + For any other state. + """ + if state not in ("ok", "warn", "error"): + raise ValueError(f"state must be ok, warn or error, not {state!r}") + self.state = state + self.setText(text) + set_flag(self, "warn", state == "warn") + set_flag(self, "error", state == "error") + + +class AdviceBox(QFrame): + """An amber box above the plots: what went wrong and what to do about it.""" + + def __init__(self) -> None: + super().__init__() + self.setObjectName("advice") + layout = QVBoxLayout(self) + layout.setContentsMargins(14, 10, 14, 10) + layout.setSpacing(2) + self.title = label("", "h3") + self.text = label("", "note", wrap=True) + self.text.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + layout.addWidget(self.title) + layout.addWidget(self.text) + self.hide() + + def say(self, title: str, text: str) -> None: + """Show the box.""" + self.title.setText(title) + self.text.setText(text) + self.show() + + +class DerivedGrid(QWidget): + """Numbers derived from the settings: a small grey name over a large value.""" + + def __init__(self, columns: int = 4) -> None: + super().__init__() + self.columns = columns + self.grid = QGridLayout(self) + self.grid.setContentsMargins(0, 0, 0, 0) + self.grid.setHorizontalSpacing(24) + self.grid.setVerticalSpacing(8) + self.values: dict[str, QLabel] = {} + + def add(self, name: str, tooltip: str = "") -> QLabel: + """Add a number called ``name`` (shown as an en dash until set).""" + index = len(self.values) + cell = QWidget() + cl = QVBoxLayout(cell) + cl.setContentsMargins(0, 0, 0, 0) + cl.setSpacing(0) + cl.addWidget(label(name, "derivedLabel")) + value = label("–", "derivedValue") + value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + cl.addWidget(value) + if tooltip: + cell.setToolTip(tooltip) + self.grid.addWidget(cell, index // self.columns, index % self.columns) + self.values[name] = value + return value + + def set(self, name: str, text: str, state: str = "ok") -> None: + """Show ``text`` for ``name``; ``state`` ``"warn"``/``"error"`` colours it.""" + value = self.values[name] + value.setText(text) + set_flag(value, "warn", state == "warn") + set_flag(value, "error", state == "error") + + def clear(self) -> None: + """Show every number as unknown.""" + for name in self.values: + self.set(name, "–") + + +class Segments(QFrame): + """A row of joined buttons choosing one of a few values. + + Parameters + ---------- + choices : sequence of (value, text) + The values and their captions, in order. + current : str + The value chosen at first. + """ + + changed = Signal(str) + + def __init__(self, choices: Sequence[tuple[str, str]], current: str) -> None: + super().__init__() + self.setObjectName("segments") + self.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed) + layout = QHBoxLayout(self) + layout.setContentsMargins(2, 2, 2, 2) + layout.setSpacing(2) + self.buttons: dict[str, QPushButton] = {} + for value, text in choices: + b = QPushButton(text) + b.setObjectName("segment") + b.setFocusPolicy(Qt.FocusPolicy.NoFocus) + b.clicked.connect(lambda _=False, v=value: self.set_value(v, emit=True)) + layout.addWidget(b) + self.buttons[value] = b + if current not in self.buttons: + raise ValueError(f"{current!r} is not one of {list(self.buttons)}") + self.value = "" + self.set_value(current) + + def set_value(self, value: str, *, emit: bool = False) -> None: + """Choose ``value``; with ``emit``, say so (only when it changed).""" + if value not in self.buttons: + raise ValueError(f"{value!r} is not one of {list(self.buttons)}") + changed = value != self.value + self.value = value + for key, b in self.buttons.items(): + set_flag(b, "current", key == value) + if emit and changed: + self.changed.emit(value) + + +class ProgressStrip(QFrame): + """A thin strip over the plots while work runs: state, bar and figures.""" + + def __init__(self) -> None: + super().__init__() + self.setObjectName("strip") + layout = QHBoxLayout(self) + layout.setContentsMargins(16, 8, 16, 8) + layout.setSpacing(14) + self.state = label("", "h3") + self.bar = QProgressBar() + self.bar.setObjectName("progress") + self.bar.setTextVisible(False) + self.bar.setRange(0, 1000) + self.figures = label("", "note") + layout.addWidget(self.state) + layout.addWidget(self.bar, stretch=1) + layout.addWidget(self.figures) + self.hide() + + def add_end(self, widget: QWidget) -> None: + """Add a widget at the strip's right end (a sparkline, a lamp, …).""" + self.layout().addWidget(widget) # pyright: ignore[reportOptionalMemberAccess] + + def show_progress(self, state: str, fraction: float, figures: str) -> None: + """Show ``state``, a bar at ``fraction`` (clamped to [0, 1]) and ``figures``.""" + self.state.setText(state) + self.bar.setValue(round(1000 * min(max(fraction, 0.0), 1.0))) + self.figures.setText(figures) + self.show() diff --git a/croak/gui/theme.py b/croak/gui/theme.py new file mode 100644 index 0000000..cf7a32c --- /dev/null +++ b/croak/gui/theme.py @@ -0,0 +1,340 @@ +"""The window's look: light and calm, with white plots, in IBM Plex Sans. + +Every colour is a named token of :class:`Palette`, and the style sheet is built +from those tokens, so one change of a token restyles every widget that uses it. +Widgets take their style from their *role*, which is their ``objectName`` +(:func:`label`, :func:`button`, :func:`frame`), and from dynamic properties such +as ``current`` or ``warn`` that :func:`set_flag` changes and re-applies. Nothing +else in the GUI calls ``setStyleSheet``. + +The look follows the solprop front end's, so the two LupoLab applications read +alike. The fonts are IBM Plex Sans, carried in ``assets/fonts`` under the SIL +Open Font License (``assets/fonts/LICENSE.txt``). +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from pathlib import Path + +from PySide6.QtGui import QFont, QFontDatabase +from PySide6.QtWidgets import QApplication, QFrame, QLabel, QPushButton, QWidget + +__all__ = [ + "FONT_FAMILY", + "PALETTE", + "Palette", + "button", + "frame", + "label", + "load_fonts", + "repolish", + "set_flag", + "stylesheet", +] + +_log = logging.getLogger(__name__) + +ASSETS = Path(__file__).parent / "assets" +"""Where the fonts and the drawn assets live.""" + +FONT_FAMILY = "IBM Plex Sans" +"""The window's font family.""" + +# The type scale, in logical pixels. Plot ticks take the application font's +# PLOT size; widgets take theirs from the style sheet. +BODY_PX = 13 +SMALL_PX = 12 +PLOT_PX = 11 +H1_PX = 28 +H2_PX = 15 +VALUE_PX = 19 + + +@dataclass(frozen=True) +class Palette: + """The window's colours, by what they are for.""" + + ink: str = "#1b2430" + """Text.""" + muted: str = "#5a6675" + """Secondary text: help, units, plot tick labels.""" + heading: str = "#3d4957" + """Small headings and unselected rail items.""" + disabled: str = "#a4adb8" + """Disabled text.""" + axis: str = "#b4bcc6" + """Plot axes.""" + accent: str = "#0b6e99" + """The primary action, focus and the selection.""" + accent_hover: str = "#095d82" + """The primary action under the pointer.""" + accent_off: str = "#b9c4cc" + """A disabled primary action.""" + selected_bg: str = "#e8f1f5" + """What is selected: its background.""" + selected_ink: str = "#0a5a7d" + """What is selected: its text; also links.""" + hover_bg: str = "#f1f3f5" + """A flat control under the pointer.""" + button_hover_bg: str = "#f6f7f8" + """A plain button under the pointer.""" + hairline: str = "#e3e6ea" + """Lines between the window's regions.""" + border: str = "#cdd3da" + """Inputs and plain buttons.""" + tile_border: str = "#dfe3e8" + """Home's tiles.""" + popup_border: str = "#d7dce2" + """Popups and dashed placeholders.""" + track: str = "#e8ebee" + """Slider grooves and progress tracks.""" + text_selection: str = "#bcdbe8" + """Selected text in an input.""" + ok: str = "#1c7a4a" + """A ready line that is ready.""" + warn: str = "#8a5200" + """A ready line with a warning.""" + error: str = "#b3261e" + """A ready line that is not ready, and errors.""" + advice_border: str = "#e6cfa8" + """The amber advice box's edge.""" + advice_bg: str = "#fdf8ee" + """The amber advice box.""" + background: str = "#ffffff" + """Everything else.""" + + +PALETTE = Palette() +"""The palette the window uses.""" + + +def stylesheet(p: Palette = PALETTE) -> str: + """Return the window's style sheet in palette ``p``. + + Parameters + ---------- + p : Palette, optional + The colours. + + Returns + ------- + str + A Qt style sheet, addressing widgets by role (object name). + """ + chevron = (ASSETS / "chevron.svg").as_posix() + input_box = ( + f"border: 1px solid {p.border}; border-radius: 5px; padding: 3px 6px;" + f" background: {p.background}; color: {p.ink};" + f" selection-background-color: {p.text_selection}; selection-color: {p.ink};" + ) + focus = f"border: 2px solid {p.accent}; padding: 2px 5px;" + flat = "border: 0; border-radius: 6px; background: none;" + selected = f"background: {p.selected_bg}; color: {p.selected_ink};" + rules = [ + # the frame + f"* {{ font-size: {BODY_PX}px; }}", + f"QMainWindow, QWidget#page, QScrollArea, QStackedWidget," + f" QScrollArea > QWidget > QWidget {{ background: {p.background}; }}", + f"QWidget#page QLabel {{ color: {p.ink}; }}", + f"QFrame#top {{ border-bottom: 1px solid {p.hairline};" + f" background: {p.background}; }}", + f"QPushButton#brand {{ border: 0; background: none; padding: 4px;" + f" font-size: {H2_PX}px; font-weight: 600; color: {p.ink}; }}", + f"QLabel#context {{ color: {p.muted}; }}", + f"QPushButton#stage {{ {flat} padding: 6px 14px; color: {p.muted};" + f" font-weight: 500; }}", + f"QPushButton#stage:hover {{ background: {p.hover_bg}; }}", + f'QPushButton#stage[current="true"] {{ {selected} }}', + f"QPushButton#stage:disabled {{ color: {p.disabled}; background: none; }}", + f"QPushButton#primary {{ border: 0; border-radius: 6px; padding: 7px 14px;" + f" min-width: 96px; background: {p.accent}; color: #fff; font-weight: 500; }}", + f"QPushButton#primary:hover {{ background: {p.accent_hover}; }}", + f"QPushButton#primary:disabled {{ background: {p.accent_off}; }}", + f"QToolButton#overflow {{ {flat} padding: 5px 8px; font-size: 16px;" + f" color: {p.muted}; }}", + f"QToolButton#overflow:hover {{ background: {p.hover_bg}; }}", + "QToolButton#overflow::menu-indicator { image: none; }", + # headings and text + f"QLabel#h1 {{ font-size: {H1_PX}px; font-weight: 600; }}", + f"QLabel#h2 {{ font-size: {H2_PX}px; font-weight: 600; }}", + f"QLabel#h3 {{ font-weight: 600; color: {p.heading}; }}", + f"QLabel#help, QLabel#note, QLabel#pageNote {{ color: {p.muted};" + f" font-size: {SMALL_PX}px; }}", + f"QLabel#control, QLabel#plotTitle {{ color: {p.heading};" + f" font-size: {SMALL_PX}px; font-weight: 500; }}", + f"QLabel#rowLabel {{ color: {p.ink}; }}", + f"QLabel#rowLabel:disabled {{ color: {p.disabled}; }}", + # Home + f"QFrame#tile {{ border: 1px solid {p.tile_border}; border-radius: 10px;" + f" background: {p.background}; min-height: 78px; }}", + f"QFrame#tile:hover {{ border-color: {p.accent}; background: #fafcfd; }}", + "QLabel#tileTitle { font-weight: 600; background: none; }", + f"QLabel#tileText {{ color: {p.muted}; background: none; }}", + "QFrame#item { border-radius: 6px; background: none; }", + f"QFrame#item:hover {{ background: {p.hover_bg}; }}", + "QLabel#itemTitle { font-weight: 500; background: none; }", + f"QLabel#itemText {{ color: {p.muted}; font-size: {SMALL_PX}px;" + " background: none; }", + # buttons + f"QPushButton#plain, QToolButton#plain {{ border: 1px solid {p.border};" + f" border-radius: 6px; padding: 5px 12px; background: {p.background};" + f" color: {p.ink}; }}", + f"QPushButton#plain:hover, QToolButton#plain:hover" + f" {{ background: {p.button_hover_bg}; }}", + f"QPushButton#plain:disabled, QToolButton#plain:disabled" + f" {{ color: {p.disabled}; }}", + "QToolButton#plain::menu-indicator { image: none; }", + f"QPushButton#link {{ border: 0; padding: 0; background: none;" + f" color: {p.selected_ink}; text-decoration: underline;" + f" font-size: {SMALL_PX}px; }}", + f"QPushButton#link:disabled {{ color: {p.disabled}; }}", + # sheets + f"QFrame#form {{ border-right: 1px solid {p.hairline};" + f" background: {p.background}; }}", + f"QToolButton#sectionHeader {{ border: 0; background: none;" + f" padding: 6px 0 4px 0; font-weight: 600; color: {p.heading}; }}", + f"QToolButton#sectionHeader:disabled {{ color: {p.disabled}; }}", + f"QLineEdit, QComboBox, QAbstractSpinBox {{ {input_box} }}", + f"QLineEdit:focus, QComboBox:focus, QAbstractSpinBox:focus {{ {focus} }}", + f"QLineEdit:disabled, QComboBox:disabled, QAbstractSpinBox:disabled" + f" {{ color: {p.disabled}; border-color: {p.hairline}; }}", + "QComboBox::drop-down { border: 0; width: 20px; }", + f'QComboBox::down-arrow {{ image: url("{chevron}");' + " width: 12px; height: 12px; }", + f"QComboBox QAbstractItemView {{ border: 1px solid {p.popup_border};" + f" background: {p.background}; color: {p.ink}; outline: 0;" + f" selection-background-color: {p.selected_bg};" + f" selection-color: {p.selected_ink}; }}", + "QAbstractSpinBox::up-button, QAbstractSpinBox::down-button" + " { width: 0; border: 0; }", + f"QCheckBox {{ color: {p.ink}; spacing: 6px; }}", + f"QCheckBox:disabled {{ color: {p.disabled}; }}", + f"QLabel#derivedLabel {{ color: {p.muted}; font-size: {SMALL_PX}px; }}", + f"QLabel#derivedValue {{ font-size: {VALUE_PX}px; font-weight: 500; }}", + f'QLabel#derivedValue[warn="true"] {{ color: {p.warn}; }}', + f'QLabel#derivedValue[error="true"] {{ color: {p.error}; }}', + f"QLabel#ready {{ color: {p.ok}; font-weight: 500; }}", + f'QLabel#ready[warn="true"] {{ color: {p.warn}; }}', + f'QLabel#ready[error="true"] {{ color: {p.error}; }}', + f"QFrame#advice {{ border: 1px solid {p.advice_border}; border-radius: 8px;" + f" background: {p.advice_bg}; }}", + f"QFrame#placeholder {{ border: 1px dashed {p.popup_border};" + f" border-radius: 8px; background: {p.background}; }}", + # sliders and progress + f"QSlider::groove:horizontal {{ height: 6px; border-radius: 3px;" + f" background: {p.track}; }}", + f"QSlider::sub-page:horizontal {{ border-radius: 3px;" + f" background: {p.accent}; }}", + f"QSlider::handle:horizontal {{ width: 16px; height: 16px; margin: -5px 0;" + f" border-radius: 8px; background: {p.accent}; }}", + f"QSlider::sub-page:horizontal:disabled, QSlider::handle:horizontal:disabled" + f" {{ background: {p.accent_off}; }}", + f"QFrame#strip {{ border-bottom: 1px solid {p.hairline};" + f" background: {p.background}; }}", + f"QProgressBar#progress {{ border: 0; border-radius: 3px;" + f" background: {p.track}; min-height: 6px; max-height: 6px;" + " color: transparent; }", + f"QProgressBar#progress::chunk {{ border-radius: 3px;" + f" background: {p.accent}; }}", + # Analyse + f"QFrame#rail {{ border-right: 1px solid {p.hairline};" + f" background: {p.background}; }}", + f"QPushButton#railItem {{ {flat} text-align: left; padding: 7px 10px;" + f" color: {p.heading}; }}", + f"QPushButton#railItem:hover {{ background: {p.hover_bg}; }}", + f'QPushButton#railItem[current="true"] {{ {selected} font-weight: 500; }}', + f"QPushButton#railItem:disabled {{ color: {p.disabled}; background: none; }}", + f"QFrame#panel {{ border-left: 1px solid {p.hairline};" + f" background: {p.background}; }}", + f"QFrame#displayBar {{ border-top: 1px solid {p.hairline};" + f" background: {p.background}; }}", + f"QFrame#segments {{ border: 1px solid {p.border}; border-radius: 6px;" + f" background: {p.background}; }}", + f"QPushButton#segment {{ border: 0; border-radius: 5px; padding: 4px 8px;" + f" background: none; color: {p.ink}; font-size: {SMALL_PX}px; }}", + f'QPushButton#segment[current="true"] {{ {selected} font-weight: 500; }}', + f"QPushButton#segment:disabled {{ color: {p.disabled}; }}", + f"QListWidget {{ border: 1px solid {p.border}; border-radius: 5px;" + f" background: {p.background}; color: {p.ink}; outline: 0; }}", + f"QListWidget::item:selected {{ {selected} }}", + # the drawer + f"QFrame#drawer {{ border-left: 1px solid {p.hairline};" + f" background: {p.background}; }}", + f"QPlainTextEdit#drawerText {{ border: 0; background: {p.background};" + f' color: {p.ink}; font-family: "Menlo", "Consolas", monospace;' + f" font-size: {SMALL_PX}px; }}", + ] + return "\n".join(rules) + + +def load_fonts() -> bool: + """Make IBM Plex Sans the application's font, the plots' ticks included. + + A missing or unreadable font file is a cosmetic failure: the window keeps + the system font and is logged, not stopped. + + Returns + ------- + bool + Whether every weight loaded. + """ + paths = sorted((ASSETS / "fonts").glob("*.ttf")) + ids = [QFontDatabase.addApplicationFont(str(path)) for path in paths] + if not ids or -1 in ids: + _log.warning("IBM Plex Sans did not load; the system font is used instead") + return False + font = QFont(FONT_FAMILY) + font.setPixelSize(PLOT_PX + 1) + QApplication.setFont(font) + return True + + +def repolish(widget: QWidget) -> None: + """Re-apply the style sheet to ``widget`` after one of its properties changed.""" + style = widget.style() + style.unpolish(widget) + style.polish(widget) + widget.update() + + +def set_flag(widget: QWidget, name: str, on: bool) -> None: + """Set the boolean style property ``name`` of ``widget`` and restyle it. + + Parameters + ---------- + widget : QWidget + The widget. + name : str + The property the style sheet tests, e.g. ``"current"``, ``"warn"``. + on : bool + Its value. + """ + if widget.property(name) == on: + return + widget.setProperty(name, on) + repolish(widget) + + +def label(text: str, role: str = "", *, wrap: bool = False) -> QLabel: + """Return a label styled as ``role`` (``"h1"``, ``"help"``, …).""" + widget = QLabel(text) + if role: + widget.setObjectName(role) + widget.setWordWrap(wrap) + return widget + + +def button(text: str, role: str = "plain") -> QPushButton: + """Return a push button styled as ``role`` (``"primary"``, ``"plain"``, …).""" + widget = QPushButton(text) + widget.setObjectName(role) + return widget + + +def frame(role: str) -> QFrame: + """Return an empty frame styled as ``role`` (``"form"``, ``"rail"``, …).""" + widget = QFrame() + widget.setObjectName(role) + return widget diff --git a/croak/gui/workflow.py b/croak/gui/workflow.py new file mode 100644 index 0000000..ec18d2e --- /dev/null +++ b/croak/gui/workflow.py @@ -0,0 +1,187 @@ +"""The window's route through a session, as rules without Qt. + +A session runs Home → Data → Prepare → Retrieve → Analyse. Which stages can be +opened, and what the one primary action in the top bar does, depend only on what +the session holds (:class:`Progress`), so they are decided here, as pure +functions the window applies and the tests check directly. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from enum import StrEnum + +__all__ = [ + "STAGES", + "Action", + "Progress", + "Stage", + "blocked_reason", + "next_stage", + "primary_action", + "remember_recent", +] + +RECENT_LIMIT = 6 +"""How many recent sessions Home lists.""" + + +class Stage(StrEnum): + """A place in the window.""" + + HOME = "home" + DATA = "data" + PREPARE = "prepare" + RETRIEVE = "retrieve" + ANALYSE = "analyse" + + @property + def title(self) -> str: + """The stage's name in the top bar.""" + return self.value.capitalize() + + +STAGES = (Stage.DATA, Stage.PREPARE, Stage.RETRIEVE, Stage.ANALYSE) +"""The stages of a session, in order (Home is outside the session).""" + + +@dataclass(frozen=True) +class Progress: + """What the current session holds, as far as the route is concerned. + + Attributes + ---------- + entry : str + ``"experimental"``, ``"simulated"`` or ``"synthetic"``; ``""`` before a + session starts. + data_ready : bool + The Data sheet can hand on a trace (a file previews, a synthetic pulse + is set up). + has_load_data : bool + A trace has been loaded (``state.load_data``). + has_tracedata : bool + A retrieval trace exists (``state.tracedata``). + has_result : bool + A retrieval result exists. + raw_direct : bool + The simulated scan goes to Retrieve on its native grid, skipping Prepare. + retrieving : bool + A retrieval is running. + estimating : bool + An uncertainty estimate is running. + """ + + entry: str = "" + data_ready: bool = False + has_load_data: bool = False + has_tracedata: bool = False + has_result: bool = False + raw_direct: bool = False + retrieving: bool = False + estimating: bool = False + + +def blocked_reason(stage: Stage, progress: Progress) -> str: + """Why ``stage`` cannot be opened now, or ``""`` if it can. + + Examples + -------- + >>> blocked_reason(Stage.PREPARE, Progress(entry="experimental")) + 'Load a trace first.' + >>> blocked_reason(Stage.DATA, Progress(entry="synthetic")) + '' + """ + if stage is Stage.HOME: + return "" + if not progress.entry: + return "Start a session from Home first." + if stage is Stage.DATA: + return "" + if stage is Stage.PREPARE: + if progress.raw_direct: + return ( + "This simulated scan is retrieved on its own grid " + "(Skip filtering & regrid), so there is nothing to prepare." + ) + return "" if progress.has_load_data else "Load a trace first." + if stage is Stage.RETRIEVE: + return "" if progress.has_tracedata else "Prepare the trace first." + return "" if progress.has_result else "Run a retrieval first." + + +def next_stage(stage: Stage, progress: Progress) -> Stage | None: + """The stage the primary action leads to from ``stage``, if it leads on. + + From Data a raw simulated scan goes straight to Retrieve. + """ + if stage is Stage.DATA: + return Stage.RETRIEVE if progress.raw_direct else Stage.PREPARE + if stage is Stage.PREPARE: + return Stage.RETRIEVE + return None + + +@dataclass(frozen=True) +class Action: + """The top bar's primary action. + + Attributes + ---------- + kind : str + What it does: ``"continue"``, ``"retrieve"``, ``"stop"``, ``"save"``, + ``"estimate"`` or ``"stop-estimate"``; ``""`` for no action. + text : str + The button's caption. + enabled : bool + Whether it can be pressed now. + """ + + kind: str + text: str + enabled: bool = True + + +def primary_action(stage: Stage, progress: Progress, page: str = "") -> Action: + """The primary action on ``stage`` (and Analyse ``page``) given ``progress``. + + Examples + -------- + >>> primary_action(Stage.RETRIEVE, Progress(entry="synthetic", retrieving=True)) + Action(kind='stop', text='Stop', enabled=True) + >>> primary_action(Stage.DATA, Progress(entry="synthetic")).enabled + False + """ + if stage is Stage.HOME: + return Action("", "", enabled=False) + if stage is Stage.DATA: + return Action("continue", "Continue", enabled=progress.data_ready) + if stage is Stage.PREPARE: + return Action("continue", "Continue", enabled=progress.has_tracedata) + if stage is Stage.RETRIEVE: + if progress.retrieving: + return Action("stop", "Stop") + text = "Retrieve again" if progress.has_result else "Retrieve" + return Action("retrieve", text, enabled=progress.has_tracedata) + if page == "uncertainty": + if progress.estimating: + return Action("stop-estimate", "Stop") + return Action("estimate", "Estimate", enabled=progress.has_result) + return Action("save", "Save", enabled=progress.has_result) + + +def remember_recent( + recent: Sequence[str], path: str, limit: int = RECENT_LIMIT +) -> list[str]: + """Put ``path`` first in the recent list, without repeats, at most ``limit``. + + Examples + -------- + >>> remember_recent(["a", "b", "c"], "b", limit=3) + ['b', 'a', 'c'] + >>> remember_recent(["a", "b"], "z", limit=2) + ['z', 'a'] + """ + if limit < 1: + raise ValueError(f"limit must be at least 1, not {limit}") + return [path, *(p for p in recent if p != path)][:limit] diff --git a/pyproject.toml b/pyproject.toml index 2ec885a..0a1beb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,9 @@ version = "0.3.1" description = "Complete retrieval of ultrashort laser pulses from FROG traces — amplitude and phase, with dispersive media, geometric smearing and uncertainty" readme = "README.md" requires-python = ">=3.14" -license = "MIT" -license-files = ["LICENSE"] +# The code is MIT; the window's font, IBM Plex Sans, is under the Open Font License. +license = "MIT AND OFL-1.1" +license-files = ["LICENSE", "croak/gui/assets/fonts/LICENSE.txt"] authors = [{ name = "John Travers" }, { name = "Christian Brahms" }] keywords = [ "ultrafast", diff --git a/tests/gui/test_plots.py b/tests/gui/test_plots.py new file mode 100644 index 0000000..dc81f60 --- /dev/null +++ b/tests/gui/test_plots.py @@ -0,0 +1,283 @@ +"""The pyqtgraph views: maps, curves, colour maps, hover readouts and views.""" + +from __future__ import annotations + +import numpy as np +import pytest + +pytest.importorskip("PySide6") + +from numpy.testing import assert_allclose +from PySide6.QtCore import QEvent, QPointF, Qt +from PySide6.QtGui import QMouseEvent + +from croak.gui.plots import ( + NEGATIVE_RGBA, + Curve, + LinePlot, + MapPlot, + Plot, + curve_reader, + is_uniform, + map_reader, + signed_colormap, +) + + +def _shown(qtbot, widget): + qtbot.addWidget(widget) + widget.resize(500, 360) + widget.show() + qtbot.waitExposed(widget) + return widget + + +def _scene_point(plot: Plot, x: float, y: float) -> QPointF: + return plot.box.mapViewToScene(QPointF(x, y)) + + +def _map(qtbot, **kwargs) -> MapPlot: + plot = _shown(qtbot, MapPlot(xlabel="Delay (fs)", ylabel="f (PHz)", **kwargs)) + x = np.linspace(-50.0, 50.0, 11) + y = np.linspace(0.6, 0.9, 7) + data = np.outer(np.arange(7.0), np.ones(11)) + plot.show_map(data, x, y, (0.0, 6.0)) + return plot + + +# -- colour maps -------------------------------------------------------------- +def test_plots_signed_colormap_is_white_at_zero_and_red_below(): + cmap = signed_colormap("viridis", -1.0, 3.0) + # 0 lies a quarter of the way along the levels. + # Qt stores colours in 8 bits, hence the tolerance of one step. + assert_allclose(cmap.map([0.25], mode=cmap.FLOAT)[0], (1, 1, 1, 1), atol=1 / 255) + assert_allclose(cmap.map([0.0], mode=cmap.FLOAT)[0], NEGATIVE_RGBA, atol=1 / 255) + + +def test_plots_unsigned_colormap_starts_white(): + cmap = signed_colormap("viridis", 0.0, 1.0) + assert_allclose(cmap.map([0.0], mode=cmap.FLOAT)[0], (1, 1, 1, 1), atol=1e-6) + assert cmap.map([1.0], mode=cmap.FLOAT)[0][3] == pytest.approx(1.0) + + +@pytest.mark.parametrize( + ("name", "low", "high", "match"), + [ + ("viridis", 0.0, 0.0, "high > 0"), + ("viridis", 2.0, 1.0, "low < high"), + ("no-such-map", 0.0, 1.0, "unknown colour map"), + ], +) +def test_plots_signed_colormap_rejects_bad_levels_and_names(name, low, high, match): + with pytest.raises(ValueError, match=match): + signed_colormap(name, low, high) + + +def test_plots_is_uniform_needs_increasing_equal_steps(): + assert is_uniform(np.linspace(0, 1, 5)) + assert not is_uniform(np.array([1.0])) + assert not is_uniform(np.array([2.0, 1.0, 0.0])) + assert not is_uniform(np.array([0.0, 1.0, 1.5])) + + +# -- maps ----------------------------------------------------------------------- +def test_plots_map_shows_data_centred_on_its_samples(qtbot): + plot = _map(qtbot) + rect = plot.image.mapRectToParent(plot.image.boundingRect()) + # 11 samples 10 fs apart, each a pixel centred on its delay. + assert rect.left() == pytest.approx(-55.0) + assert rect.width() == pytest.approx(110.0) + assert plot.bar.levels() == pytest.approx((0.0, 6.0)) + assert plot.value_at(0.0, 0.6) == 0.0 + assert plot.value_at(10.0, 0.9) == 6.0 + assert plot.value_at(500.0, 0.7) is None + + +@pytest.mark.parametrize( + ("data", "x", "match"), + [ + (np.zeros((3, 3)), np.linspace(0, 1, 4), "does not match"), + (np.zeros((7, 3)), np.array([0.0, 1.0, 3.0]), "evenly spaced"), + ], +) +def test_plots_map_rejects_mismatched_or_uneven_axes(qtbot, data, x, match): + plot = _shown(qtbot, MapPlot()) + with pytest.raises(ValueError, match=match): + plot.show_map(data, x, np.linspace(0, 1, 7), (0.0, 1.0)) + + +def test_plots_map_updates_in_place_and_changes_levels_freely(qtbot): + plot = _map(qtbot) + plot.update_map(np.full((7, 11), 2.0)) + assert plot.value_at(0.0, 0.6) == 2.0 + with pytest.raises(ValueError, match="shape already shown"): + plot.update_map(np.zeros((2, 2))) + plot.set_levels((-40.0, 0.0)) + assert plot.bar.levels() == pytest.approx((-40.0, 0.0)) + + +def test_plots_signed_map_recolours_for_negative_levels(qtbot): + plot = _map(qtbot, signed=True) + plot.set_levels((-2.0, 6.0)) + cmap = plot.bar.colorMap() + assert_allclose(cmap.map([0.0], mode=cmap.FLOAT)[0], NEGATIVE_RGBA, atol=1 / 255) + + +def test_plots_map_says_a_message_in_place_of_the_image(qtbot): + plot = _map(qtbot) + plot.say("No trace loaded") + assert plot.data is None + assert plot.message.isVisible() + assert plot.value_at(0.0, 0.7) is None + plot.show_map(np.ones((2, 2)), np.arange(2.0), np.arange(2.0), (0.0, 1.0)) + assert not plot.message.isVisible() + + +def test_plots_map_update_needs_an_image_first(qtbot): + plot = _shown(qtbot, MapPlot()) + with pytest.raises(ValueError, match="shape already shown"): + plot.update_map(np.zeros((2, 2))) + + +# -- hover and view --------------------------------------------------------------- +def test_plots_hover_reads_the_map_under_the_pointer(qtbot): + plot = _map(qtbot) + plot.reader = map_reader(plot, "τ", "f") + assert plot.reader(500.0, 0.65) == "" # off the image: nothing to say + plot.hover(_scene_point(plot, -40.0, 0.65)) + assert plot.note.isVisible() and plot.cross_x.isVisible() + assert plot.note.toPlainText().strip() == "τ -40 f 0.65 1" + # Past 62 % of the width the readout flips to the pointer's left. + plot.hover(_scene_point(plot, 40.0, 0.92)) + assert plot.note.anchor.x() == pytest.approx(1.0) + assert plot.note.anchor.y() == pytest.approx(0.0) + + +def test_plots_hover_hides_outside_the_box_without_a_reader_or_text(qtbot): + plot = _map(qtbot) + plot.hover(_scene_point(plot, 0.0, 0.7)) # no reader yet + assert not plot.note.isVisible() + plot.reader = lambda _x, _y: "" + plot.hover(_scene_point(plot, 0.0, 0.7)) + assert not plot.note.isVisible() + plot.reader = map_reader(plot, "τ", "f") + plot.hover(QPointF(-1000.0, -1000.0)) + assert not plot.note.isVisible() + plot.hover(_scene_point(plot, 0.0, 0.7)) + plot.leaveEvent(QEvent(QEvent.Type.Leave)) + assert not plot.cross_y.isVisible() + + +def test_plots_double_click_returns_to_the_home_view(qtbot): + plot = _shown(qtbot, Plot(xlabel="x", ylabel="y")) + event = QMouseEvent( + QEvent.Type.MouseButtonDblClick, + QPointF(10, 10), + QPointF(10, 10), + Qt.MouseButton.LeftButton, + Qt.MouseButton.LeftButton, + Qt.KeyboardModifier.NoModifier, + ) + plot.mouseDoubleClickEvent(event) # no home yet: fits whatever is there + plot.set_view((0.0, 10.0), (-1.0, 1.0)) + plot.box.setRange(xRange=(4.0, 5.0), padding=0) + plot.mouseDoubleClickEvent(event) + assert plot.box.viewRange()[0] == pytest.approx([0.0, 10.0]) + plot.set_labels("Delay (fs)", None) + assert plot.item.getAxis("bottom").labelText == "Delay (fs)" + plot.set_labels(None, "Power (W)") + assert plot.item.getAxis("left").labelText == "Power (W)" + + +# -- lines -------------------------------------------------------------------------- +def test_plots_lines_take_colours_in_turn_and_a_legend_when_several(qtbot): + plot = _shown(qtbot, LinePlot(xlabel="t (fs)", ylabel="I")) + x = np.linspace(-1, 1, 21) + plot.set_curves([Curve(x, x**2)]) + assert plot.legend is None + plot.set_curves( + [ + Curve(x, x**2, "retrieved"), + Curve(x, x**2 + 1, "truth", colour="#000000", dashed=True), + Curve(x, x**3, "fit"), + ] + ) + assert plot.legend is not None and plot.legend.isVisible() + assert len(plot.legend.items) == 3 + pens = [item.opts["pen"] for item in plot.items] + assert pens[0].color().name() == "#1f6fb4" + assert pens[1].style() == Qt.PenStyle.DashLine + # The explicitly coloured curve does not use up a palette colour. + assert pens[2].color().name() == "#d2670b" + plot.set_curves([Curve(x, x)]) + assert not plot.legend.isVisible() + + +def test_plots_log_axis_drops_non_positive_values(qtbot): + plot = _shown(qtbot, LinePlot()) + x = np.arange(5.0) + plot.set_curves([Curve(x, np.array([0.0, 1.0, 10.0, -1.0, 100.0]))], log_y=True) + assert_allclose(plot.items[0].yData, [1.0, 10.0, 100.0]) + plot.update_curve(0, x, np.array([1.0, 0.0, 1.0, 1.0, 1.0])) + assert plot.items[0].yData.size == 4 + plot.fit() + # log10 of 1 … 1: a flat line gets a unit-wide window around it. + assert plot.box.viewRange()[1] == pytest.approx([-0.05, 0.05]) + + +def test_plots_right_axis_carries_its_own_curves(qtbot): + plot = _shown(qtbot, LinePlot(ylabel="I", right_label="Phase (rad)")) + x = np.linspace(0, 1, 11) + plot.set_curves([Curve(x, x, "I"), Curve(x, -x, "φ", right=True)], log_y=True) + assert plot.right_box is not None + assert plot.items[1] in plot.right_box.addedItems + # Negative phase survives the log intensity axis. + assert plot.items[1].yData.min() == pytest.approx(-1.0) + plot.set_right_range(-3.0, 3.0) + assert plot.right_box.viewRange()[1] == pytest.approx([-3.0, 3.0]) + plot.set_curves([Curve(x, x)]) # removes the right-hand curve again + assert plot.items[0] not in plot.right_box.addedItems + plot.box.resize(300, 200) # the right view box follows the main one + + +def test_plots_right_range_is_ignored_without_a_right_axis(qtbot): + plot = _shown(qtbot, LinePlot()) + plot.set_right_range(0.0, 1.0) + assert plot.right_box is None + + +def test_plots_marks_replace_the_previous_set(qtbot): + plot = _shown(qtbot, LinePlot()) + plot.set_marks([1.0, 2.0]) + plot.set_marks([3.0], colour="#ff0000") + assert [m.value() for m in plot.marks] == [3.0] + + +def test_plots_fit_frames_the_left_curves(qtbot): + plot = _shown(qtbot, LinePlot()) + plot.fit() # nothing to fit + x = np.linspace(0.0, 2.0, 3) + plot.set_curves([Curve(x, np.array([0.0, 10.0, 0.0])), Curve(x, x, right=True)]) + plot.fit() + assert plot.box.viewRange()[0] == pytest.approx([0.0, 2.0]) + assert plot.box.viewRange()[1] == pytest.approx([-0.5, 10.5]) + plot.set_curves([Curve(x, np.full(3, np.nan))]) + plot.fit() # no finite value: a default window + assert plot.box.viewRange()[1] == pytest.approx([-0.05, 1.05]) + + +def test_plots_curve_reader_reads_each_named_left_curve(qtbot): + plot = _shown(qtbot, LinePlot()) + x = np.linspace(0.0, 1.0, 11) + plot.set_curves( + [ + Curve(x, 2 * x, "R"), + Curve(x, x), + Curve(x, x, "φ", right=True), + Curve(x + 5, x, "far"), + Curve(np.zeros(0), np.zeros(0), "empty"), + ] + ) + read = curve_reader(plot, "t") + assert read(0.5, 0.0) == "t 0.5 R 1 0.5" + assert read(-3.0, 0.0) == "" diff --git a/tests/gui/test_sheet.py b/tests/gui/test_sheet.py new file mode 100644 index 0000000..8c0a4c6 --- /dev/null +++ b/tests/gui/test_sheet.py @@ -0,0 +1,226 @@ +"""Sheet building blocks: sections, rows, help, find, reflow and status widgets.""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("PySide6") + +from PySide6.QtWidgets import QCheckBox, QLabel, QLineEdit, QSpinBox, QWidget + +from croak.gui import settings +from croak.gui.sheet import ( + LABEL_WIDTH, + NARROW_WIDTH, + AdviceBox, + DerivedGrid, + Form, + ProgressStrip, + ReadyLine, + Row, + Segments, + Sheet, +) + +HELP = [ + ("Grid", [("Points", "How many samples."), ("Resample t", "Resample in time.")]), + ("Filtering", [("Threshold", "Clip below this.")]), + ("Unused", [("Nothing", "No control has this label.")]), +] + + +def _form(qtbot, **kwargs) -> Form: + form = Form(HELP, **kwargs) + qtbot.addWidget(form) + return form + + +def test_sheet_rows_take_their_help_from_section_and_label(qtbot): + form = _form(qtbot) + grid = form.add_section("Grid") + points = grid.add_row("Points", QSpinBox()) + resample = grid.add_row("ignored", QCheckBox("Resample t")) + loose = grid.add_row("Undocumented", QLineEdit()) + assert points.editor.toolTip() == "How many samples." + assert points.label is not None and points.label.width() == LABEL_WIDTH + # A check box is its own label and is keyed by its text. + assert resample.label is None + assert resample.text == "Resample t" + assert resample.editor.toolTip() == "Resample in time." + assert loose.editor.toolTip() == "" + assert form.matched == {("Grid", "Points"), ("Grid", "Resample t")} + + +def test_sheet_check_box_with_escaped_ampersand_finds_its_help(qtbot): + form = Form([("S", [("Skip filtering & regrid", "Raw.")])]) + qtbot.addWidget(form) + row = form.add_section("S").add_row("", QCheckBox("Skip filtering && regrid")) + assert row.help_text == "Raw." + + +def test_sheet_row_label_greys_with_its_row(qtbot): + row = Row("Points", QSpinBox()) + qtbot.addWidget(row) + row.setEnabled(False) + assert row.label is not None and not row.label.isEnabled() + row.setEnabled(True) + assert row.label.isEnabled() + box = Row("", QCheckBox("Flag")) + qtbot.addWidget(box) + box.setEnabled(False) # a check box row has no separate label to grey + + +def test_sheet_focused_row_shows_its_help_line(qtbot): + form = _form(qtbot) + form.show() + qtbot.waitExposed(form) + spin = QSpinBox() + form.add_section("Grid").add_row("Points", spin) + spin.setFocus() + qtbot.waitUntil(lambda: form.help.text() == "How many samples.") + other = QLineEdit() + form.add_widget(other) # outside any row: the help line keeps its text + other.setFocus() + qtbot.waitUntil(other.hasFocus) + assert form.help.text() == "How many samples." + + +def test_sheet_help_ignores_rows_of_another_form(qtbot): + form, other = _form(qtbot), _form(qtbot) + spin = QSpinBox() + other.add_section("Grid").add_row("Points", spin) + form._focus_moved(None, spin) + assert form.help.text() == "" + form._focus_moved(None, None) + + +def test_sheet_sections_fold_and_remember_it(qtbot): + form = _form(qtbot) + section = form.add_section("Grid", folded=True, settings_key="test/grid_open") + assert section.folded and section.body.isHidden() + section.set_folded(False) + assert not section.folded + assert settings.value("test/grid_open", False) is True + second = _form(qtbot) # held, so Python does not delete the widget + again = second.add_section("Grid", folded=True, settings_key="test/grid_open") + assert not again.folded # the remembered state wins over the default + plain = form.add_section("Filtering") + plain.set_folded(True) # no key: nothing remembered + plain.add_widget(QLabel("extra")) + assert plain.folded + + +def test_sheet_find_shows_only_matching_rows(qtbot): + form = _form(qtbot, find=True) + form.show() + grid = form.add_section("Grid") + points = grid.add_row("Points", QSpinBox()) + resample = grid.add_row("", QCheckBox("Resample t")) + filtering = form.add_section("Filtering", folded=True) + threshold = filtering.add_row("Threshold", QSpinBox()) + assert form.find is not None + form.find.setText("clip") # matches the threshold's help text + assert threshold.isVisible() and filtering.body.isVisible() + assert not grid.isVisible() + form.find.setText("samples") + assert points.isVisible() and not resample.isVisible() + form.find.setText("") + assert grid.isVisible() and resample.isVisible() + assert filtering.folded and not filtering.body.isVisible() + + +def test_sheet_form_without_find_has_no_box(qtbot): + form = Form() + qtbot.addWidget(form) + assert form.find is None + assert form.help_map == {} + + +def test_sheet_plots_move_below_the_form_on_a_narrow_window(qtbot): + form, plots = _form(qtbot), QWidget() + sheet = Sheet(form, plots) + qtbot.addWidget(sheet) + sheet.resize(NARROW_WIDTH + 200, 700) + sheet.show() + qtbot.waitExposed(sheet) + assert not sheet.narrow + sheet.resize(NARROW_WIDTH - 200, 700) + qtbot.waitUntil(lambda: plots.geometry().top() >= form.geometry().bottom()) + assert sheet.narrow + sheet.resize(NARROW_WIDTH - 100, 700) # still narrow: no change + qtbot.wait(10) + assert sheet.narrow + + +@pytest.mark.parametrize( + ("state", "warn", "error"), + [("ok", False, False), ("warn", True, False), ("error", False, True)], +) +def test_sheet_ready_line_colours_its_state(qtbot, state, warn, error): + line = ReadyLine() + qtbot.addWidget(line) + line.say("Ready", state) + assert line.text() == "Ready" and line.state == state + assert bool(line.property("warn")) is warn + assert bool(line.property("error")) is error + + +def test_sheet_ready_line_rejects_an_unknown_state(qtbot): + line = ReadyLine() + qtbot.addWidget(line) + with pytest.raises(ValueError, match="ok, warn or error"): + line.say("?", "maybe") + + +def test_sheet_advice_box_appears_when_it_has_something_to_say(qtbot): + box = AdviceBox() + qtbot.addWidget(box) + assert box.isHidden() + box.say("Retrieval failed", "Try a smaller grid.") + assert not box.isHidden() + assert box.title.text() == "Retrieval failed" + + +def test_sheet_derived_numbers_show_values_and_states(qtbot): + grid = DerivedGrid(columns=2) + qtbot.addWidget(grid) + grid.add("FWHM") + grid.add("Edge energy", tooltip="Energy at the grid's edge.") + grid.add("Grid") + assert grid.grid.itemAtPosition(1, 0) is not None # wrapped onto row 2 + grid.set("Edge energy", "2 %", "warn") + assert grid.values["Edge energy"].property("warn") is True + grid.clear() + assert grid.values["FWHM"].text() == "–" + assert grid.values["Edge energy"].property("warn") is False + + +def test_sheet_segments_choose_one_value(qtbot): + seg = Segments( + [("frequency", "Frequency"), ("wavelength", "Wavelength")], "frequency" + ) + qtbot.addWidget(seg) + with qtbot.waitSignal(seg.changed) as blocker: + seg.buttons["wavelength"].click() + assert blocker.args == ["wavelength"] + assert seg.buttons["wavelength"].property("current") is True + assert seg.buttons["frequency"].property("current") is False + with qtbot.assertNotEmitted(seg.changed): + seg.buttons["wavelength"].click() # no change: no signal + seg.set_value("frequency") # programmatic: no signal + with pytest.raises(ValueError, match="not one of"): + seg.set_value("time") + with pytest.raises(ValueError, match="not one of"): + Segments([("a", "A")], "b") + + +def test_sheet_progress_strip_clamps_its_bar(qtbot): + strip = ProgressStrip() + qtbot.addWidget(strip) + assert strip.isHidden() + strip.add_end(QLabel("sparkline")) + strip.show_progress("Retrieving", 1.7, "iteration 10") + assert strip.bar.value() == 1000 and not strip.isHidden() + strip.show_progress("Retrieving", -1.0, "") + assert strip.bar.value() == 0 + assert strip.state.text() == "Retrieving" diff --git a/tests/gui/test_theme.py b/tests/gui/test_theme.py new file mode 100644 index 0000000..b8f88d3 --- /dev/null +++ b/tests/gui/test_theme.py @@ -0,0 +1,86 @@ +"""The window's look: palette, style sheet, fonts and styled widgets.""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("PySide6") + +from PySide6.QtGui import QFontDatabase +from PySide6.QtWidgets import QApplication, QWidget + +from croak.gui import theme + + +def test_theme_stylesheet_is_built_from_the_palette(): + sheet = theme.stylesheet() + assert theme.PALETTE.accent in sheet + assert "QPushButton#primary" in sheet + # Every token reaches the sheet only through the palette: a new palette + # recolours every rule that used the old value. + recoloured = theme.stylesheet(theme.Palette(accent="#123456")) + assert "#123456" in recoloured + assert theme.PALETTE.accent not in recoloured + # The combo chevron points at a file that ships with the package. + assert (theme.ASSETS / "chevron.svg").is_file() + + +def test_theme_fonts_load_and_become_the_application_font(qapp): + assert theme.load_fonts() + assert QApplication.font().family() == theme.FONT_FAMILY + assert theme.FONT_FAMILY in QFontDatabase.families() + assert (theme.ASSETS / "fonts" / "LICENSE.txt").is_file() + + +@pytest.mark.parametrize("found", [[], [0, -1]]) +def test_theme_missing_fonts_fall_back_to_the_system_font( + qapp, monkeypatch, tmp_path, caplog, found +): + fonts = tmp_path / "fonts" + fonts.mkdir() + for index in range(len(found)): + (fonts / f"f{index}.ttf").write_bytes(b"") + ids = iter(found) + monkeypatch.setattr(theme, "ASSETS", tmp_path) + monkeypatch.setattr( + theme.QFontDatabase, "addApplicationFont", lambda _path: next(ids) + ) + before = QApplication.font() + with caplog.at_level("WARNING", logger="croak.gui.theme"): + assert not theme.load_fonts() + assert "system font" in caplog.text + assert QApplication.font() == before + + +def test_theme_set_flag_restyles_only_on_change(qtbot, monkeypatch): + widget = QWidget() + qtbot.addWidget(widget) + calls = [] + monkeypatch.setattr(theme, "repolish", calls.append) + theme.set_flag(widget, "current", True) + theme.set_flag(widget, "current", True) + assert widget.property("current") is True + assert calls == [widget] + + +def test_theme_repolish_reapplies_the_style(qtbot): + widget = QWidget() + qtbot.addWidget(widget) + widget.setStyleSheet(theme.stylesheet()) + theme.repolish(widget) # must not raise on a styled widget + + +def test_theme_builders_set_the_role(qtbot): + heading = theme.label("Hello", "h1") + plain = theme.label("text", wrap=True) + primary = theme.button("Go", "primary") + plain_button = theme.button("Open") + box = theme.frame("form") + for widget in (heading, plain, primary, plain_button, box): + qtbot.addWidget(widget) + assert heading.objectName() == "h1" + assert plain.objectName() == "" + assert plain.wordWrap() + assert primary.objectName() == "primary" + assert plain_button.objectName() == "plain" + assert box.objectName() == "form" diff --git a/tests/gui/test_workflow.py b/tests/gui/test_workflow.py new file mode 100644 index 0000000..0599b7d --- /dev/null +++ b/tests/gui/test_workflow.py @@ -0,0 +1,106 @@ +"""The window's route rules: which stages open and what the primary action does.""" + +from __future__ import annotations + +import pytest + +from croak.gui.workflow import ( + STAGES, + Action, + Progress, + Stage, + blocked_reason, + next_stage, + primary_action, + remember_recent, +) + +FRESH = Progress(entry="experimental") +LOADED = Progress(entry="experimental", data_ready=True, has_load_data=True) +PREPARED = Progress( + entry="experimental", data_ready=True, has_load_data=True, has_tracedata=True +) +RETRIEVED = Progress( + entry="experimental", + data_ready=True, + has_load_data=True, + has_tracedata=True, + has_result=True, +) + + +def test_workflow_stages_are_in_session_order_with_titles(): + assert [s.title for s in STAGES] == ["Data", "Prepare", "Retrieve", "Analyse"] + assert Stage.HOME not in STAGES + + +@pytest.mark.parametrize( + ("stage", "progress", "blocked"), + [ + (Stage.HOME, Progress(), False), + (Stage.DATA, Progress(), True), # no session yet + (Stage.DATA, FRESH, False), + (Stage.PREPARE, FRESH, True), + (Stage.PREPARE, LOADED, False), + (Stage.RETRIEVE, LOADED, True), + (Stage.RETRIEVE, PREPARED, False), + (Stage.ANALYSE, PREPARED, True), + (Stage.ANALYSE, RETRIEVED, False), + ], +) +def test_workflow_stage_opens_only_once_its_input_exists(stage, progress, blocked): + assert bool(blocked_reason(stage, progress)) is blocked + + +def test_workflow_raw_simulated_scan_skips_prepare_with_a_reason(): + raw = Progress(entry="simulated", has_load_data=True, raw_direct=True) + assert "Skip filtering & regrid" in blocked_reason(Stage.PREPARE, raw) + assert next_stage(Stage.DATA, raw) is Stage.RETRIEVE + + +@pytest.mark.parametrize( + ("stage", "expected"), + [ + (Stage.DATA, Stage.PREPARE), + (Stage.PREPARE, Stage.RETRIEVE), + (Stage.RETRIEVE, None), + (Stage.ANALYSE, None), + ], +) +def test_workflow_next_stage_follows_the_session(stage, expected): + assert next_stage(stage, LOADED) is expected + + +@pytest.mark.parametrize( + ("stage", "progress", "page", "expected"), + [ + (Stage.HOME, FRESH, "", Action("", "", enabled=False)), + (Stage.DATA, FRESH, "", Action("continue", "Continue", enabled=False)), + (Stage.DATA, LOADED, "", Action("continue", "Continue")), + (Stage.PREPARE, LOADED, "", Action("continue", "Continue", enabled=False)), + (Stage.PREPARE, PREPARED, "", Action("continue", "Continue")), + (Stage.RETRIEVE, LOADED, "", Action("retrieve", "Retrieve", enabled=False)), + (Stage.RETRIEVE, PREPARED, "", Action("retrieve", "Retrieve")), + (Stage.RETRIEVE, RETRIEVED, "", Action("retrieve", "Retrieve again")), + (Stage.ANALYSE, RETRIEVED, "overview", Action("save", "Save")), + (Stage.ANALYSE, PREPARED, "", Action("save", "Save", enabled=False)), + (Stage.ANALYSE, RETRIEVED, "uncertainty", Action("estimate", "Estimate")), + ], +) +def test_workflow_primary_action_is_what_comes_next(stage, progress, page, expected): + assert primary_action(stage, progress, page) == expected + + +def test_workflow_primary_action_stops_running_work(): + running = Progress(entry="synthetic", has_tracedata=True, retrieving=True) + assert primary_action(Stage.RETRIEVE, running).kind == "stop" + estimating = Progress(entry="synthetic", has_result=True, estimating=True) + action = primary_action(Stage.ANALYSE, estimating, "uncertainty") + assert action == Action("stop-estimate", "Stop") + + +def test_workflow_recent_list_moves_a_path_first_and_caps_its_length(): + assert remember_recent([], "a") == ["a"] + assert remember_recent(["a", "b", "c"], "c", limit=2) == ["c", "a"] + with pytest.raises(ValueError, match="at least 1"): + remember_recent(["a"], "b", limit=0) From 998804db077cd43204360b62f510b1fce2a5f552 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 21:37:25 +0000 Subject: [PATCH 03/41] Document croak.plotting's re-exported constants again autodoc reads a constant's #: doc-comment only from the module it documents, so after the package split the constants re-exported by croak/plotting/__init__.py (RETRIEVAL_PANELS, EDGE_ENERGY_WARN, ...) vanished from the API page and their cross-references stopped resolving, silently. Seed autodoc's analyzer for the package with the submodules' comments; the rendered API page now matches the pre-split one plus the new module map. Co-Authored-By: Claude Opus 5.5 --- croak/plotting.py | 2661 --------------------------------------------- docs/conf.py | 29 + 2 files changed, 29 insertions(+), 2661 deletions(-) delete mode 100644 croak/plotting.py diff --git a/croak/plotting.py b/croak/plotting.py deleted file mode 100644 index 6bbf5c1..0000000 --- a/croak/plotting.py +++ /dev/null @@ -1,2661 +0,0 @@ -"""Matplotlib plotting for retrieval results. - -Provides custom colormaps (white at the low end; a red band for negatives), -reusable **single-axis** plotters (so the GUI embeds exactly the same code), and -**composite** figures: the 12-panel :func:`plot_retrieval`, the 6-panel -:func:`plot_frog_filter`, and :func:`plot_simulated_trace`. - -The retrieval summary is assembled from twelve independent **panels** -(``draw_(ax, data)`` over one :class:`RetrievalPlotData` bundle, registered in -:data:`RETRIEVAL_PANELS`). :func:`plot_retrieval` lays them out as the 3×4 -overview; :func:`plot_retrieval_page` draws one of the three 2×2 pages -(:data:`RETRIEVAL_PAGES`: Traces, Pulse, Diagnostics) the GUI shows as tabs. - -Figures are built with :external+matplotlib:class:`matplotlib.figure.Figure` directly -(no ``pyplot`` global state), so they embed cleanly in a Qt canvas and also save -standalone via ``fig.savefig(...)``. -""" - -from __future__ import annotations - -import contextlib -import functools -from collections.abc import Callable, Mapping, Sequence -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Literal - -import numpy as np -from matplotlib.colors import ListedColormap -from matplotlib.figure import Figure -from numpy.typing import ArrayLike - -from .maths import wlfreq -from .processing import ( - ProcessedResult, - TruthPulse, - marginals, - process_result, - spectrogram, -) -from .result import RetrievalResult - -if TYPE_CHECKING: - from matplotlib.axes import Axes - from matplotlib.collections import QuadMesh - from matplotlib.colors import Colormap - from matplotlib.legend import Legend - from matplotlib.lines import Line2D - from matplotlib.text import Text - from numpy.typing import NDArray - - from .uncertainty import UncertaintyResult - -#: Edge-energy fraction (see :func:`croak.processing.edge_energy_fraction`) above -#: which the spectrum panel flags the retrieval in red. A sound retrieval on a -#: generous band sits two orders of magnitude below this; above it, re-run on a -#: wider grid. -EDGE_ENERGY_WARN = 0.01 - -__all__ = [ - "PlotScale", - "FULL_SCALE", - "FONT_SCALE_MIN", - "FONT_SCALE_STEP", - "COMPACT_BELOW", - "plot_scale", - "scaled_rc", - "cmap_white", - "cmap_negwhite", - "signed_pcolormesh", - "si_power", - "sig3", - "plot_residual", - "plot_temporal", - "plot_spectral", - "SPECTRAL_AXES", - "EDGE_ENERGY_WARN", - "plot_convergence", - "plot_marginal", - "plot_spectrogram", - "plot_retrieval", - "RetrievalPlotData", - "retrieval_plot_data", - "TraceArtists", - "ResidualArtists", - "TemporalArtists", - "SpectralArtists", - "ConvergenceArtists", - "LineArtists", - "MarginalArtists", - "SpectrogramArtists", - "PanelSpec", - "PageSpec", - "ColorbarSpec", - "RETRIEVAL_PANELS", - "RETRIEVAL_COLORBARS", - "RETRIEVAL_PAGES", - "RETRIEVAL_OVERVIEW", - "plot_retrieval_page", - "draw_measured_log", - "draw_retrieved_log", - "draw_measured_lin", - "draw_retrieved_lin", - "draw_temporal", - "draw_spectral", - "draw_convergence", - "draw_residual", - "draw_spectral_filter", - "draw_freq_marginal", - "draw_delay_marginal", - "draw_spectrogram", - "update_measured_log", - "update_retrieved_log", - "update_measured_lin", - "update_retrieved_lin", - "update_temporal", - "update_spectral", - "update_convergence", - "update_convergence_curve", - "update_residual", - "update_spectral_filter", - "update_freq_marginal", - "update_delay_marginal", - "update_spectrogram", - "RetrievalPageView", - "plot_frog_filter", - "filter_view_vmax", - "FrogFilterView", - "plot_simulated_trace", - "plot_fwhm_distribution", - "plot_intensity_band", - "plot_uncertainty", -] - -_TWOPI_PHZ = 2 * np.pi * 1e15 - -#: Abscissae the spectrum panel can be drawn against (see :func:`plot_spectral`). -SPECTRAL_AXES: tuple[str, ...] = ("frequency", "wavelength") -type SpectralAxis = Literal["frequency", "wavelength"] - - -def sig3(value: float) -> str: - """Format a number to three significant digits, for a read-out. - - Three significant digits keeps a read-out equally informative across the - ranges croak covers (sub-fs attosecond pulses through hundreds of fs; - nanometre-wide through hundred-nanometre-wide spectra), where a fixed number - of decimals is too coarse at one end and spuriously precise at the other. - Unlike the ``g`` format this keeps trailing zeros (so ``7.00``, not ``7``) and - never switches to exponent notation, both of which read badly beside a sibling - entry in a legend or a table column. - - Parameters - ---------- - value : float - The number to format, already in its display unit. - - Returns - ------- - str - The formatted number, with no unit attached. - - Examples - -------- - >>> sig3(9.512), sig3(7.0), sig3(0.85), sig3(953.2) - ('9.51', '7.00', '0.850', '953') - """ - if not np.isfinite(value) or value == 0.0: - return f"{value:.2f}" - # digits before the decimal point (<= 0 for |value| < 1), so 3 - that many is - # how many decimals are still needed to reach three significant digits. - integer_digits = int(np.floor(np.log10(abs(value)))) + 1 - return f"{value:.{max(0, 3 - integer_digits)}f}" - - -def _fs(seconds: float) -> str: - """Format a duration (s) as femtoseconds to three significant digits.""" - return sig3(seconds / 1e-15) - - -# --------------------------------------------------------------------------- -# On-screen density -# --------------------------------------------------------------------------- -# Matplotlib sizes text in points, so when a figure is squeezed into a small window -# the axes shrink and the text does not: on a 1366x768 laptop the twelve-panel -# retrieval summary spent 78 % of its area on titles, labels and padding. The GUI -# canvas (croak.gui.canvas.MplCanvas) therefore scales the rc font sizes by how far -# the canvas has fallen below the size its figure was designed for. The scale is a -# pure function of geometry, kept here so the plotting layer stays Qt-free and the -# thresholds are testable headlessly. - -#: Smallest fraction of the design font size that text may shrink to. Below ~70 % -#: a 10 pt label is 7 pt, the limit of comfortable legibility on a laptop panel. -FONT_SCALE_MIN = 0.7 -#: Quantisation step of :func:`plot_scale`. A window resize re-plots only when the -#: scale crosses a step, so a few pixels of drag never trigger a redraw. -FONT_SCALE_STEP = 0.05 -#: Below this scale :attr:`PlotScale.compact` is set, for plotting code that wants -#: to drop optional decoration (a second legend entry, a colorbar label) when tight. -COMPACT_BELOW = 0.9 - - -@dataclass(frozen=True) -class PlotScale: - """Text density of a figure relative to the size its layout was designed for. - - Parameters - ---------- - font : float - Multiplier applied to every rc font size (and text padding), in - ``[FONT_SCALE_MIN, 1.0]``. ``1.0`` is matplotlib's default 10 pt base. - - Examples - -------- - >>> PlotScale(0.85).compact - True - >>> FULL_SCALE.compact - False - """ - - font: float = 1.0 - - @property - def compact(self) -> bool: - """Whether the figure is tight enough to drop optional decoration.""" - return self.font < COMPACT_BELOW - - -#: The unscaled density: matplotlib's defaults. -FULL_SCALE = PlotScale() - - -def plot_scale( - width_in: float, height_in: float, design_in: tuple[float, float] -) -> PlotScale: - """Return the text density for a figure of the given size. - - The scale is the fraction of the design size that fits in the *tighter* - direction, ``min(width / design_width, height / design_height)``, quantised - to :data:`FONT_SCALE_STEP` and clamped to ``[FONT_SCALE_MIN, 1.0]``. A figure - at or above its design size is drawn at full scale; one squeezed to 60 % of - its design height gets the minimum. - - Parameters - ---------- - width_in, height_in : float - Current figure size in inches (``Figure.get_size_inches()``). On a Qt - canvas this is the logical pixel size / 100 on every display, because - matplotlib folds the device-pixel ratio into ``figure.dpi``. - design_in : tuple of float - ``(width, height)`` in inches the figure's layout was designed for — the - ``figsize`` a stage passes to its canvas. - - Returns - ------- - PlotScale - - Raises - ------ - ValueError - If any size is not positive. - - Examples - -------- - >>> plot_scale(9.91, 4.21, (12.0, 7.0)).font # Retrieve page at 1366x768 - 0.7 - >>> plot_scale(15.45, 7.33, (12.0, 7.0)).font # ... at 1920x1080 - 1.0 - """ - design_w, design_h = design_in - if min(width_in, height_in, design_w, design_h) <= 0: - raise ValueError( - f"figure and design sizes must be positive, got {width_in}x{height_in} " - f"in for a {design_w}x{design_h} in design" - ) - ratio = min(width_in / design_w, height_in / design_h) - # Quantise to the nearest step; round to two decimals so equal steps compare - # equal as floats (0.85, not 0.8500000000000001). - quantised = round(round(ratio / FONT_SCALE_STEP) * FONT_SCALE_STEP, 2) - return PlotScale(font=min(1.0, max(FONT_SCALE_MIN, quantised))) - - -# Matplotlib's default sizes spelled out in points. The rc defaults for the title -# and label keys are the *relative* strings 'large'/'medium', which cannot be -# multiplied; writing the resolved points also makes the scaling immune to a user -# matplotlibrc with absolute sizes. The four pads are included because text -# *padding* is a fixed-pixel cost too. -_SCALED_RC_BASE: dict[str, float] = { - "font.size": 10.0, - "axes.titlesize": 12.0, - "axes.labelsize": 10.0, - "xtick.labelsize": 10.0, - "ytick.labelsize": 10.0, - "legend.fontsize": 10.0, - "figure.titlesize": 12.0, - "axes.labelpad": 4.0, - "axes.titlepad": 6.0, - "xtick.major.pad": 3.5, - "ytick.major.pad": 3.5, -} - - -def scaled_rc(scale: PlotScale) -> dict[str, float]: - """Return rcParams overrides that shrink every font size and pad by ``scale``. - - Use with :external+matplotlib:func:`matplotlib.rc_context` *around the plotting - call*: matplotlib resolves relative sizes (``fontsize="small"``, the ``'large'`` - title default) against ``font.size`` when an artist is created, not when it is - drawn, so the context must enclose everything that creates text. - - Parameters - ---------- - scale : PlotScale - The density; :data:`FULL_SCALE` reproduces matplotlib's defaults. - - Returns - ------- - dict - ``{rc key: value in points}`` for the font-size and padding keys. - - Examples - -------- - >>> round(scaled_rc(PlotScale(0.7))["axes.titlesize"], 2) - 8.4 - """ - return {key: base * scale.font for key, base in _SCALED_RC_BASE.items()} - - -# --------------------------------------------------------------------------- -# Colormaps -# --------------------------------------------------------------------------- -def cmap_white(base: str = "viridis", *, n: int = 8, white: bool = True, N: int = 512): - """Build a base colormap with (optionally) a white point at the low end.""" - import matplotlib as mpl - - cm = mpl.colormaps[base] - vals = np.linspace(0, 1, n) - cols = cm(vals) - if white: - cols[0] = [1, 1, 1, 1] - xi = np.linspace(0, 1, N) - out = np.empty((N, 4)) - for k in range(4): - out[:, k] = np.interp(xi, vals, cols[:, k]) - return ListedColormap(out) - - -def cmap_negwhite( - base: str = "viridis", - *, - n: int = 8, - white: bool = True, - negfrac: float = 0.0, - N: int = 512, -): - """Like :func:`cmap_white` but with a red gradient for negative values.""" - if negfrac <= 0: - return cmap_white(base, n=n, white=white, N=N) - n_pos = max(round(N * (1 - negfrac)), 2) - n_neg = N - n_pos - pos = cmap_white(base, n=n, white=white, N=n_pos)(np.linspace(0, 1, n_pos)) - neg = np.empty((n_neg, 4)) - for i in range(n_neg): - t = i / max(n_neg - 1, 1) - neg[i] = [1.0, t, t, 1.0] - return ListedColormap(np.vstack([neg, pos])) - - -def _white_red(): - """White→red colormap used to render negative trace values by magnitude.""" - from matplotlib.colors import LinearSegmentedColormap - - return LinearSegmentedColormap.from_list("white_red", ["white", "red"]) - - -def _signed_magnitude( - C, *, db: bool, tracedb: float, vmax: float | None -) -> tuple[NDArray[np.float64], NDArray[np.bool_], float, float]: - """Split ``C`` into magnitude, negative mask and the shared ``(vmin, vmax)``. - - One code path for drawing and for updating in place. - """ - C = np.asarray(C, dtype=float) - neg = C < 0 - if db: - mag = np.clip(10 * np.log10(np.maximum(np.abs(C), 1e-30)), -tracedb, 0.0) - return mag, neg, -tracedb, 0.0 - mag = np.abs(C) - return mag, neg, 0.0, float(np.nanmax(mag)) if vmax is None else vmax - - -def signed_pcolormesh(ax, x, y, C, *, db, tracedb=30.0, cmap_pos, vmax=None): - """Pcolormesh that splits an image's sign onto two colormaps. - - Positive samples are drawn with ``cmap_pos`` (e.g. viridis); negative - samples are drawn by *magnitude* with a white→red map. Both maps share the - **same** scale (``[0, vmax]`` linear, ``[-tracedb, 0]`` dB), so a negative is - only as saturated as a positive of equal magnitude — small negatives stay - faint instead of dominating — and zero is white in both (pair ``cmap_pos`` - with :func:`cmap_white`). ``C`` follows the usual - :external+matplotlib:meth:`~matplotlib.axes.Axes.pcolormesh` convention — - shape ``(len(y), len(x))`` (a FROG ``(Nλ, Nτ)`` array already has this shape - for x = delay and y = wavelength). - - Returns ``(mappable_pos, mappable_neg)`` (the second is ``None`` when there - are no negative samples). - """ - mag, neg, vmin, vhi = _signed_magnitude(C, db=db, tracedb=tracedb, vmax=vmax) - mpos = ax.pcolormesh( - x, - y, - np.ma.masked_where(neg, mag), - vmin=vmin, - vmax=vhi, - cmap=cmap_pos, - rasterized=True, - shading="auto", - ) - mneg = None - if neg.any(): - mneg = ax.pcolormesh( - x, - y, - np.ma.masked_where(~neg, mag), - vmin=vmin, - vmax=vhi, - cmap=_white_red(), - rasterized=True, - shading="auto", - ) - return mpos, mneg - - -# --------------------------------------------------------------------------- -# Single-axis plotters (reused by the composites and the GUI) -# --------------------------------------------------------------------------- -def plot_residual(ax, omega, delays, resid, omega0, *, halfwidth=None, flim=None): - """Plot the measured−retrieved residual with a symmetric blue-white-red map. - - ``flim`` optionally bounds the frequency axis, as ``(f_min, f_max)`` in Hz. - """ - mr = float(np.max(np.abs(resid))) or 1.0 - freq = (np.asarray(omega) + omega0) / _TWOPI_PHZ - m = ax.pcolormesh( - np.asarray(delays) / 1e-15, - freq, - resid, - vmin=-mr, - vmax=mr, - cmap=cmap_white("bwr", n=32, white=False), - rasterized=True, - shading="auto", - ) - ax.set_xlabel("Delay (fs)") - ax.set_ylabel("Frequency (PHz)") - ax.set_title("Residuals") - if halfwidth is not None: - ax.set_xlim(-halfwidth / 1e-15, halfwidth / 1e-15) - if flim is not None: - ax.set_ylim(flim[0] / 1e15, flim[1] / 1e15) - return m - - -_POWER_PREFIXES: tuple[tuple[float, str], ...] = ( - (1e15, "PW"), - (1e12, "TW"), - (1e9, "GW"), - (1e6, "MW"), - (1e3, "kW"), - (1.0, "W"), - (1e-3, "mW"), - (1e-6, "µW"), -) - - -def si_power(watts: float) -> tuple[float, str]: - """Return ``(scale, unit)`` to display ``watts`` with an SI power prefix. - - Picks the largest prefix not exceeding ``|watts|`` (so e.g. ``1.2e7`` W → - ``(1e6, "MW")``). Non-finite or non-positive inputs fall back to plain watts; - values below the smallest prefix use that smallest prefix. - """ - w = abs(float(watts)) - if not np.isfinite(w) or w <= 0: - return 1.0, "W" - for scale, unit in _POWER_PREFIXES: - if w >= scale: - return scale, unit - return _POWER_PREFIXES[-1] - - -def _prepare_fig(fig, figsize): - """Return a cleared figure to draw a composite into (new if None).""" - if fig is None: - return Figure(figsize=figsize, layout="constrained") - fig.clear() - with contextlib.suppress(Exception): - fig.set_layout_engine("constrained") - return fig - - -def _combined_legend(ax, axp, *, loc: str) -> Legend: - """Draw one legend containing intensity and twin-axis phase curves. - - ``loc`` is a fixed corner: matplotlib's ``"best"`` re-evaluates on every draw - and made the legend jump between frames of the live preview. - """ - handles, labels = ax.get_legend_handles_labels() - phase_handles, phase_labels = axp.get_legend_handles_labels() - return ax.legend( - handles + phase_handles, - labels + phase_labels, - loc=loc, - handlelength=1.0, - frameon=False, - fontsize="small", - ) - - -# --------------------------------------------------------------------------- -# Panel artists -# --------------------------------------------------------------------------- -# Each retrieval panel returns the matplotlib artists it created, in a small frozen -# dataclass, so that a live view can later update them in place (set_data, -# set_array, set_text) instead of clearing and rebuilding the figure, and so that -# page-level decorations (the colorbars shared by a pair of trace panels) can find -# their mappables. eq=False: comparing artists by field value is never meaningful. - - -@dataclass(frozen=True, eq=False) -class TraceArtists: - """Artists of a trace-image panel. - - Attributes - ---------- - mesh_pos, mesh_neg : QuadMesh - The positive-sample image and, when the data hold negatives, the - white→red magnitude image of the negative samples (else ``None``). - label : Text or None - The bold error/shape annotation of the retrieved-linear panel. - """ - - mesh_pos: QuadMesh - mesh_neg: QuadMesh | None - label: Text | None = None - - -@dataclass(frozen=True, eq=False) -class ResidualArtists: - """Artists of the residual panel: its signed image (``None`` if no residual).""" - - mesh: QuadMesh | None - - -@dataclass(frozen=True, eq=False) -class TemporalArtists: - """Artists of the temporal panel. - - Attributes - ---------- - line_retr, line_tl : Line2D - Retrieved and transform-limited intensity (or power) curves. - line_phase : Line2D - Retrieved temporal phase on the twin axis ``ax_phase``. - ax_phase : Axes - The right-hand phase axis. - legend : Legend - The combined intensity + phase legend. - legend_retr, legend_tl : Text - The legend entries carrying the R and TL durations (they change with the - result, so a live update rewrites them in place). - line_truth, line_truth_phase : Line2D or None - Ground-truth intensity and phase overlays, when a truth was given. - """ - - line_retr: Line2D - line_tl: Line2D - line_phase: Line2D - ax_phase: Axes - legend: Legend - legend_retr: Text - legend_tl: Text - line_truth: Line2D | None = None - line_truth_phase: Line2D | None = None - - -@dataclass(frozen=True, eq=False) -class SpectralArtists: - """Artists of the spectrum panel. - - Attributes - ---------- - line_retr : Line2D - Retrieved spectral intensity. - line_phase, line_phase_fit : Line2D - Retrieved spectral phase and its dashed polynomial fit, on ``ax_phase``. - ax_phase : Axes - The right-hand phase axis. - legend : Legend - The combined intensity + phase legend. - text_dispersion, text_edge : Text - The GDD/TOD block and the edge-energy warning. - line_meas, line_truth, line_truth_phase : Line2D or None - Measured spectrum, truth spectrum and truth phase, when available. - """ - - line_retr: Line2D - line_phase: Line2D - line_phase_fit: Line2D - ax_phase: Axes - legend: Legend - text_dispersion: Text - text_edge: Text - line_meas: Line2D | None = None - line_truth: Line2D | None = None - line_truth_phase: Line2D | None = None - - -@dataclass(frozen=True, eq=False) -class ConvergenceArtists: - """Artists of the convergence panel: the error curve, stage rules, legend.""" - - line: Line2D - boundaries: tuple[Line2D, ...] = () - legend: Legend | None = None - - -@dataclass(frozen=True, eq=False) -class LineArtists: - """A single-curve panel (the spectral filter).""" - - line: Line2D - - -@dataclass(frozen=True, eq=False) -class MarginalArtists: - """Artists of a marginal panel: retrieved and (optional) measured curves.""" - - line_retr: Line2D - legend: Legend - line_meas: Line2D | None = None - - -@dataclass(frozen=True, eq=False) -class SpectrogramArtists: - """Artists of the spectrogram panel: its image.""" - - mesh: QuadMesh - - -type PanelArtists = ( - TraceArtists - | ResidualArtists - | TemporalArtists - | SpectralArtists - | ConvergenceArtists - | LineArtists - | MarginalArtists - | SpectrogramArtists -) - - -def _temporal_curves(pr: ProcessedResult, power_scale): - """Return ``(retr, tl, r_label, tl_label, ylabel, power_scale)`` for the pulse. - - In absolute power when ``pr.peak_power`` is known (deriving the SI prefix if - ``power_scale`` is None), else normalised. Shared by draw and update. - """ - if pr.peak_power is not None: - if power_scale is None: - power_scale = si_power(max(pr.peak_power, pr.peak_power_tl or 0.0)) - scale, unit = power_scale - retr = pr.It_retr * pr.peak_power / scale - tl = pr.It_tl * (pr.peak_power_tl or 0.0) / scale - r_label = f"R ({_fs(pr.fwhm_retr)} fs, {pr.peak_power / scale:.1f} {unit})" - tl_label = ( - f"TL ({_fs(pr.fwhm_tl)} fs, {(pr.peak_power_tl or 0.0) / scale:.1f} {unit})" - ) - return retr, tl, r_label, tl_label, f"Power ({unit})", power_scale - r_label = f"R ({_fs(pr.fwhm_retr)} fs)" - tl_label = f"TL ({_fs(pr.fwhm_tl)} fs)" - return pr.It_retr, pr.It_tl, r_label, tl_label, "Power (a.u.)", None - - -def _truth_intensity(truth: TruthPulse, energy, power_scale) -> NDArray[np.float64]: - """Return the truth's intensity on the temporal panel's scale. - - With a pulse energy the truth is shown in absolute power on the shared SI - scale: it carries the same energy, so its peak power is energy/∫(I/I_peak)dt - over its *own* duration (:meth:`~croak.processing.TruthPulse.peak_power`). - """ - peak_power = truth.peak_power(energy) - if peak_power is None: - return np.asarray(truth.It, dtype=float) - scale = power_scale[0] if power_scale is not None else 1.0 - return np.asarray(truth.It, dtype=float) * peak_power / scale - - -def _intensity_ylim(*curves) -> tuple[float, float]: - """``(0, 1.05 × max)`` over the given curves (``None`` entries ignored). - - Set explicitly because ``Axes.set_ylim`` switches autoscaling off, so an - in-place update could not rely on it; the 5 % headroom is matplotlib's - default margin. - """ - tops = [float(np.nanmax(c)) for c in curves if c is not None and np.size(c)] - top = max(tops, default=1.0) - return 0.0, 1.05 * top if top > 0 else 1.0 - - -def _phase_ylim(*curves) -> tuple[float, float] | None: - """Phase-axis limits centred on the shown phases with their span as margin. - - Finite samples of every non-empty curve count; ``None`` when nothing is shown. - """ - arrays = [np.asarray(c, dtype=float) for c in curves if c is not None] - arrays = [a[np.isfinite(a)] for a in arrays] - arrays = [a for a in arrays if a.size] - if not arrays: - return None - pm = np.concatenate(arrays) - d = pm.max() - pm.min() - mid = (pm.max() + pm.min()) / 2 - return mid - d - 1e-9, mid + d + 1e-9 - - -def _temporal_panel( - ax, - pr: ProcessedResult, - *, - halfwidth=None, - power_scale=None, - truth: TruthPulse | None = None, -) -> TemporalArtists: - """Draw the temporal panel into ``ax``; return its artists (see plot_temporal).""" - t = pr.t_over / 1e-15 - retr, tl, r_label, tl_label, ylabel, power_scale = _temporal_curves(pr, power_scale) - (line_retr,) = ax.plot(t, retr, c="C0", label=r_label) - # The TL pulse is peak-centred on its own oversampled axis (its peak index - # need not coincide with the retrieved pulse's), so plot it against t_tl — - # using t_over would shift its peak off t=0 by a sample or two. - (line_tl,) = ax.plot(pr.t_tl / 1e-15, tl, c="C1", label=tl_label) - line_truth = None - truth_curve = None - if truth is not None: - # The known pulse as a thin black line *under* the retrieved/TL curves, in - # physical units so it is independent of the retrieval grid. A retrieval - # is defined only up to the time-direction ambiguity, so the truth may - # appear shifted or flipped. Its legend entry carries its own FWHM so the - # three durations read side by side. - truth_curve = _truth_intensity(truth, pr.energy, power_scale) - (line_truth,) = ax.plot( - truth.t / 1e-15, - truth_curve, - c="k", - lw=0.8, - zorder=1, - label=f"truth ({_fs(truth.fwhm)} fs)", - ) - ax.set_xlabel("Time (fs)") - ax.set_ylabel(ylabel) - ax.set_ylim(*_intensity_ylim(retr, tl, truth_curve)) - ax.set_title("Retrieved pulse") - if halfwidth is not None: - ax.set_xlim(-halfwidth / 1e-15, halfwidth / 1e-15) - axp = ax.twinx() - (line_phase,) = axp.plot(t[pr.mask_t], pr.phi_t[pr.mask_t], c="C2") - axp.set_ylabel("Phase (rad)") - line_truth_phase = None - truth_phase = None - if truth is not None and truth.phi_t is not None: - truth_mask = np.asarray(truth.It) > 0.01 - truth_phase = np.asarray(truth.phi_t)[truth_mask] - (line_truth_phase,) = axp.plot( - np.asarray(truth.t)[truth_mask] / 1e-15, - truth_phase, - ":", - c="0.35", - lw=1.2, - label="truth phase", - ) - limits = _phase_ylim(pr.phi_t[pr.mask_t], truth_phase) - if limits is not None: - axp.set_ylim(*limits) - # Upper left: the pulse and its masked phase sit at the centre, the wings are - # empty, and a fixed corner cannot jump between live frames. - legend = _combined_legend(ax, axp, loc="upper left") - texts = legend.get_texts() - return TemporalArtists( - line_retr=line_retr, - line_tl=line_tl, - line_phase=line_phase, - ax_phase=axp, - legend=legend, - legend_retr=texts[0], - legend_tl=texts[1], - line_truth=line_truth, - line_truth_phase=line_truth_phase, - ) - - -def plot_temporal( - ax, - pr: ProcessedResult, - *, - halfwidth=None, - power_scale=None, - truth: TruthPulse | None = None, -): - """Temporal intensity/power (left) and phase (right twin axis); phase masked. - - When ``pr.peak_power`` is set (a measured pulse energy was supplied to - :func:`~croak.processing.process_result`), the intensity is shown as absolute - instantaneous power on an SI-prefixed axis; otherwise it stays normalised - ("a.u."). ``power_scale`` is an optional ``(scale, unit)`` pair (see - :func:`si_power`) so several panels can share one prefix; when ``None`` it is - derived from the larger of the retrieved/TL peak powers. - - ``truth`` (a :class:`~croak.processing.TruthPulse`: a synthetic generating - field, or a simulated scan's reference intensity) is overlaid as a thin black - line under the retrieved/TL curves, labelled with its own FWHM, and — when it - carries a temporal phase — as a dotted phase in the same display gauge as the - retrieved one (see :func:`croak.processing.normalize_temporal_phase`). - - Returns the twin phase axes. :func:`draw_temporal` is the same panel for the - registry, returning every artist (:class:`TemporalArtists`). - """ - return _temporal_panel( - ax, pr, halfwidth=halfwidth, power_scale=power_scale, truth=truth - ).ax_phase - - -def _truth_spectral_phase( - wavelength: NDArray[np.float64], truth: TruthPulse | None -) -> NDArray[np.float64] | None: - """Return the truth's spectral phase interpolated onto ``wavelength``, or None. - - The truth is stored on the simulation's own frequency grid, which is neither - the same spacing nor the same extent as the retrieval's; NaN outside it. - """ - tphi_src = getattr(truth, "phi_w", None) if truth is not None else None - tlam_src = getattr(truth, "lam", None) if truth is not None else None - if tphi_src is None or tlam_src is None: - return None - return np.interp( - wavelength, - np.asarray(tlam_src, dtype=float), - np.asarray(tphi_src, dtype=float), - left=np.nan, - right=np.nan, - ) - - -def _dispersion_text(pr: ProcessedResult) -> str: - """Return the GDD/TOD annotation of the spectrum panel.""" - return f"GDD: {pr.gdd_fs2:.1f} fs²\nTOD: {pr.tod_fs3:.1f} fs³" - - -def _edge_text(pr: ProcessedResult) -> tuple[str, str]: - """Return the edge-energy annotation and its colour (red once it matters). - - Energy parked in the unmeasured grid-edge bins is an artefact indicator; the - 1 % threshold sits comfortably above what a sound retrieval produces. - """ - color = "C3" if pr.edge_energy > EDGE_ENERGY_WARN else "0.35" - return f"Edge: {100 * pr.edge_energy:.2f} %", color - - -def _unit_peak(values: NDArray[np.float64]) -> NDArray[np.float64]: - """Scale ``values`` to a peak of one (NaNs ignored; all-zero left alone).""" - peak = float(np.nanmax(values)) if values.size else 0.0 - return values / peak if peak > 0 else values - - -def _spectral_curves(pr: ProcessedResult, axis: str): - """Return ``(x, retrieved, measured, xlabel)`` of the spectrum panel on ``axis``. - - Both spectra are unit-peak densities *in the chosen variable*. On the - wavelength axis they are ``pr.Ilam`` and ``pr.Iw_meas`` — both wavelength - densities (the measured one carries the ω² Jacobian despite its name); on the - frequency axis the retrieved spectrum is ``|E(ω)|²`` itself and the measured - one has that Jacobian divided out again, so a spectrum symmetric in ω plots - symmetric. ``x`` is per grid index on both axes, so the phase curves index it - with ``pr.mask_w`` unchanged. - """ - if axis == "wavelength": - return pr.wavelength / 1e-9, pr.Ilam, pr.Iw_meas, "Wavelength (nm)" - if axis != "frequency": - raise ValueError(f"axis must be one of {SPECTRAL_AXES}, got {axis!r}") - omega_abs = pr.omega + pr.omega0 - measured = None - if pr.Iw_meas is not None: - # A centred grid can cross ω = 0; that sample is undefined as a density. - with np.errstate(divide="ignore", invalid="ignore"): - jacobian = np.where(omega_abs != 0, 1.0 / omega_abs**2, np.nan) - measured = _unit_peak(pr.Iw_meas * jacobian) - return omega_abs / _TWOPI_PHZ, _unit_peak(pr.Iw), measured, "Frequency (PHz)" - - -def _spectral_xlim(lam_min, lam_max, axis: str) -> tuple[float, float] | None: - """Return the spectrum panel's x-limits for a wavelength window on ``axis``.""" - if not (lam_min and lam_max): - return None - if axis == "wavelength": - return lam_min / 1e-9, lam_max / 1e-9 - return float(wlfreq(lam_max)) / _TWOPI_PHZ, float(wlfreq(lam_min)) / _TWOPI_PHZ - - -def _truth_spectrum(truth: TruthPulse, axis: str): - """Return the truth's spectrum ``(x, unit-peak density)`` on ``axis``, or None.""" - if truth.lam is None or truth.Iw is None: - return None - if axis == "wavelength": - return truth.lam / 1e-9, truth.Iw - omega = wlfreq(truth.lam) - return omega / _TWOPI_PHZ, _unit_peak(np.asarray(truth.Iw) / omega**2) - - -def _spectral_panel( - ax, - pr: ProcessedResult, - *, - lam_min=None, - lam_max=None, - truth: TruthPulse | None = None, - axis: str = "frequency", -) -> SpectralArtists: - """Draw the spectrum panel into ``ax``; return its artists (see plot_spectral).""" - x, retrieved, measured, xlabel = _spectral_curves(pr, axis) - (line_retr,) = ax.plot(x, retrieved, c="C0", label="R") - line_meas = None - if measured is not None: - (line_meas,) = ax.plot(x, measured, c="C1", label="M") - line_truth = None - truth_curve = None if truth is None else _truth_spectrum(truth, axis) - if truth_curve is not None: - (line_truth,) = ax.plot(*truth_curve, c="k", lw=0.8, zorder=1, label="truth") - ax.set_xlabel(xlabel) - ax.set_ylabel("Power (a.u.)") - ax.set_ylim(0, 1.05) - ax.set_title("Spectrum") - xlim = _spectral_xlim(lam_min, lam_max, axis) - if xlim is not None: - ax.set_xlim(*xlim) - axp = ax.twinx() - (line_phase,) = axp.plot(x[pr.mask_w], pr.phi_w[pr.mask_w], c="C2") - # dashed polynomial fit of the spectral phase (GDD/TOD visualisation) - (line_phase_fit,) = axp.plot( - x[pr.mask_w], pr.phi_w_fit[pr.mask_w], "--", c="C4", alpha=0.7 - ) - axp.set_ylabel("Phase (rad)") - tphi = _truth_spectral_phase(pr.wavelength, truth) - line_truth_phase = None - if tphi is not None: - (line_truth_phase,) = axp.plot( - x[pr.mask_w], - tphi[pr.mask_w], - ":", - c="0.35", - lw=1.2, - label="truth phase", - ) - limits = _phase_ylim(pr.phi_w[pr.mask_w], None if tphi is None else tphi[pr.mask_w]) - if limits is not None: - axp.set_ylim(*limits) - text_dispersion = ax.text( - 0.05, 0.78, _dispersion_text(pr), transform=ax.transAxes, fontsize="small" - ) - edge, edge_color = _edge_text(pr) - text_edge = ax.text( - 0.05, 0.70, edge, transform=ax.transAxes, fontsize="small", color=edge_color - ) - # Upper right: the GDD/TOD and edge annotations occupy the upper left. - legend = _combined_legend(ax, axp, loc="upper right") - return SpectralArtists( - line_retr=line_retr, - line_phase=line_phase, - line_phase_fit=line_phase_fit, - ax_phase=axp, - legend=legend, - text_dispersion=text_dispersion, - text_edge=text_edge, - line_meas=line_meas, - line_truth=line_truth, - line_truth_phase=line_truth_phase, - ) - - -def plot_spectral( - ax, - pr: ProcessedResult, - *, - lam_min=None, - lam_max=None, - truth: TruthPulse | None = None, - axis: SpectralAxis = "frequency", -): - """Spectral intensity (left) and phase (right twin axis) against ``axis``. - - ``axis`` is ``"frequency"`` (PHz, the default: the space the retrieval works in, - where ``|E(ω)|²`` of a symmetric pulse is symmetric) or ``"wavelength"`` (nm, the - spectrometer's axis, carrying the 1/λ² density Jacobian). Both spectra are drawn as - unit-peak densities in the chosen variable; ``lam_min``/``lam_max`` (m) bound the - axis on either. The phase curves are the same on both. - - ``truth``, when it carries a spectrum, is overlaid as a thin black line under - the retrieved curve; when it carries a known spectral phase, that phase is - overlaid on the phase axis. It is drawn in the SAME gauge as the retrieved - phase (unwrapped, de-tilted, zeroed at the spectral peak — see - :func:`croak.processing.normalize_spectral_phase`), because spectral phase - is defined only up to a constant and a linear term; a truth pinned any other - way would show a tilt or offset that is gauge, not error. - - Returns the twin phase axes. :func:`draw_spectral` is the same panel for the - registry, returning every artist (:class:`SpectralArtists`). - """ - return _spectral_panel( - ax, pr, lam_min=lam_min, lam_max=lam_max, truth=truth, axis=axis - ).ax_phase - - -def plot_convergence( - ax, errors: ArrayLike, *, boundaries: Sequence[int] = () -) -> ConvergenceArtists: - """Plot the FROG error vs iteration on a log y-axis. - - ``boundaries`` are indices into ``errors`` where a multi-stage solver handed - over to its next stage (:attr:`~croak.result.RetrievalResult.stage_boundaries`); - each is drawn as a dashed rule. Marking them matters because the stages need - not count the same unit of work — ``warm-lbfgs`` splices COPRA iterations onto - L-BFGS function evaluations — so an unmarked kink would read as convergence - behaviour rather than as the change of algorithm it is. - - The error curve is created even for an empty ``errors`` (a result restored - from file carries no history), so a live view can grow it in place. - """ - errors = np.asarray(errors, dtype=float) - (line,) = ax.semilogy(np.arange(1, errors.size + 1), errors, c="C0") - rules: list[Line2D] = [] - for i, edge in enumerate(boundaries): - if 0 < edge < errors.size: - rules.append( - ax.axvline( - edge + 0.5, - c="0.5", - ls="--", - lw=0.8, - label="stage handover" if i == 0 else None, - ) - ) - legend = None - if rules: # upper right: a decreasing curve leaves that corner empty - legend = ax.legend( - loc="upper right", handlelength=1.2, frameon=False, fontsize="small" - ) - ax.set_xlabel("Iteration") - ax.set_ylabel("Error") - ax.set_title("Convergence") - return ConvergenceArtists(line=line, boundaries=tuple(rules), legend=legend) - - -def plot_marginal( - ax, x, retrieved, measured=None, *, xlabel="", scale=1.0, halfwidth=None -) -> MarginalArtists: - """Plot a (retrieved vs measured) trace marginal.""" - (line_retr,) = ax.plot(np.asarray(x) / scale, retrieved, c="C0", label="R") - line_meas = None - if measured is not None: - (line_meas,) = ax.plot(np.asarray(x) / scale, measured, c="C1", label="M") - ax.set_xlabel(xlabel) - ax.set_ylabel("Marginal (a.u.)") - ax.set_ylim(0, 1.05) - if halfwidth is not None: - ax.set_xlim(-halfwidth / scale, halfwidth / scale) - legend = ax.legend( - loc="upper left", handlelength=1.0, frameon=False, fontsize="small" - ) - return MarginalArtists(line_retr=line_retr, legend=legend, line_meas=line_meas) - - -def plot_spectrogram( - ax, - t_centers, - wavelength, - S, - *, - lam_min=None, - lam_max=None, - halfwidth=None, - cmap=None, -): - """Plot a Gabor spectrogram (time fs × wavelength nm).""" - if cmap is None: - cmap = cmap_white("viridis") - m = ax.pcolormesh( - t_centers / 1e-15, - wavelength / 1e-9, - S, - cmap=cmap, - rasterized=True, - shading="auto", - ) - ax.set_xlabel("Time (fs)") - ax.set_ylabel("Wavelength (nm)") - ax.set_title("Spectrogram") - if lam_min and lam_max: - ax.set_ylim(lam_min / 1e-9, lam_max / 1e-9) - if halfwidth is not None: - ax.set_xlim(-halfwidth / 1e-15, halfwidth / 1e-15) - return m - - -# --------------------------------------------------------------------------- -# Retrieval panels: the data bundle, the twelve panels, pages and the overview -# --------------------------------------------------------------------------- -# The twelve-panel retrieval summary is built from independent panels so the GUI -# can arrange them as three 2×2 pages (Traces / Pulse / Diagnostics) while scripts -# and the CLI keep the 3×4 overview. Every panel is a pure function -# ``draw_(ax, data)`` over one immutable RetrievalPlotData, computed once per -# result. Colorbars that span two panels are a page-level concern (ColorbarSpec), -# drawn after all panels of a figure exist. - - -@dataclass(frozen=True, eq=False) -class RetrievalPlotData: - """Everything the retrieval panels share, computed once from one result. - - Build it with :func:`retrieval_plot_data`. The bundle is immutable and carries - no figure state, so one instance can feed several figures (the GUI's pages and - their pop-outs) and be redrawn at any size. - - Attributes - ---------- - result : RetrievalResult - The retrieval; ``result.trace`` is the retrieved trace. - processed : ProcessedResult - Display-ready quantities of ``result`` - (:func:`croak.processing.process_result`). - measured, retrieved : ndarray - Measured and retrieved traces, shape ``(Nω, Nτ)``. - omega0_trace : float - Centre angular frequency of the *signal* (twice ``omega0`` for SHG), so - the trace panels' frequency axis reads at the signal frequency. - halfwidth : float - Half the delay span (s); bounds the delay axes. - lam_min, lam_max : float - Wavelength bounds (m) of the spectrum and spectrogram panels. - flim : tuple of float or None - Optional ``(f_min, f_max)`` (Hz) bounds of the trace/residual frequency axes. - tracedb : float - Dynamic range (dB) of the logarithmic trace panels. - cmap_pos : Colormap - White-based positive colormap shared by the trace and spectrogram panels. - lin_vmax : float - Common scale of the linear trace pair, so their colorbar is meaningful. - truth : TruthPulse or None - Known pulse to overlay (synthetic and simulated data). - power_scale : tuple of (float, str) or None - SI ``(scale, unit)`` for absolute power when a pulse energy was given. - spectral_axis : str - ``"frequency"`` or ``"wavelength"``: the spectrum panel's abscissa. - """ - - result: RetrievalResult - processed: ProcessedResult - measured: NDArray[np.float64] - retrieved: NDArray[np.float64] - omega0_trace: float - halfwidth: float - lam_min: float - lam_max: float - flim: tuple[float, float] | None - tracedb: float - cmap_pos: Colormap - lin_vmax: float - truth: TruthPulse | None - power_scale: tuple[float, str] | None - spectral_axis: str - - @functools.cached_property - def spectrogram( - self, - ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: - """``(t_centres, wavelength, S)`` of :func:`croak.processing.spectrogram`. - - The Gabor transform is the costliest thing the panels compute (hundreds - of FFTs), so it runs only when the spectrogram panel is first drawn and is - then cached on the bundle for every later draw of it. - """ - return spectrogram(self.result, lam_min=self.lam_min, lam_max=self.lam_max) - - -def retrieval_plot_data( - result: RetrievalResult, - *, - measured: ArrayLike | None = None, - Iomega_meas: ArrayLike | None = None, - lam_min: float | None = None, - lam_max: float | None = None, - flim: tuple[float, float] | None = None, - tracedb: float = 30.0, - cmap: str = "viridis", - truth: TruthPulse | None = None, - energy: float | None = None, - processed: ProcessedResult | None = None, - spectral_axis: str = "frequency", -) -> RetrievalPlotData: - """Bundle a retrieval's plot inputs for the panels (see :func:`plot_retrieval`). - - The arguments are those of :func:`plot_retrieval`, which is this bundle drawn - as the overview; see there for their meaning. ``processed`` is an - already-computed :class:`~croak.processing.ProcessedResult` **for this same - result** (the GUI computes it once for its read-out); it must have been built - with the same ``measured``/``Iomega_meas``/``energy``. - - Raises - ------ - ValueError - If ``result`` carries no trace, ``processed`` belongs to another result, - or ``spectral_axis`` is not one of :data:`SPECTRAL_AXES`. - """ - omega = result.omega - omega0 = result.omega0 - if spectral_axis not in SPECTRAL_AXES: - raise ValueError( - f"spectral_axis must be one of {SPECTRAL_AXES}, got {spectral_axis!r}" - ) - if result.trace is None: - raise ValueError("plot_retrieval requires a result carrying a simulated trace") - retrieved = np.asarray(result.trace, dtype=float) - measured_arr = retrieved if measured is None else np.asarray(measured, dtype=float) - - if processed is None: - pr = process_result( - result, measured=measured_arr, Iomega_meas=Iomega_meas, energy=energy - ) - elif processed.result is not result: - # Cheap exact guard: mixing a ProcessedResult from another result would - # draw the trace panels from one retrieval and the pulse/spectrum panels - # from another, which is very hard to spot by eye. - raise ValueError("processed must be the ProcessedResult of this result") - else: - pr = processed - if lam_min is None or lam_max is None: - lams = wlfreq(omega + omega0) - lam_min, lam_max = float(lams.min()), float(lams.max()) - delays = result.delays - # When a pulse energy was supplied, derive one SI power prefix for the - # temporal panel (and the truth overlay) so both share a consistent axis. - power_scale = None - if pr.peak_power is not None: - power_scale = si_power(max(pr.peak_power, pr.peak_power_tl or 0.0)) - return RetrievalPlotData( - result=result, - processed=pr, - measured=measured_arr, - retrieved=retrieved, - omega0_trace=omega0 * _interaction_scale(result.interaction), - halfwidth=float((delays.max() - delays.min()) / 2), - lam_min=float(lam_min), - lam_max=float(lam_max), - flim=flim, - tracedb=tracedb, - # Positive values on the base map with white at the low end, so zero is - # white in *both* maps and meets the white→red negative map seamlessly. - cmap_pos=cmap_white(cmap), - # The linear pair shares one scale so its colorbar is meaningful. - lin_vmax=max( - float(np.nanmax(np.abs(measured_arr))), float(np.nanmax(retrieved)) - ), - truth=truth, - power_scale=power_scale, - spectral_axis=spectral_axis, - ) - - -def _trace_label(data: RetrievalPlotData) -> str: - """Return the retrieved-linear panel's annotation: FROG error and shape.""" - return ( - f"{data.result.error * 100:.2f}%\n" - f"{data.retrieved.shape[0]}×{data.retrieved.shape[1]}" - ) - - -def _draw_trace_image( - ax, data: RetrievalPlotData, C: NDArray[np.float64], *, db: bool, title: str -) -> tuple[QuadMesh, QuadMesh | None]: - """Draw one trace image (delay × signal frequency) and return its meshes.""" - mpos, mneg = signed_pcolormesh( - ax, - data.result.delays / 1e-15, - (data.result.omega + data.omega0_trace) / _TWOPI_PHZ, - C, - db=db, - tracedb=data.tracedb, - cmap_pos=data.cmap_pos, - vmax=None if db else data.lin_vmax, - ) - ax.set_xlabel("Delay (fs)") - ax.set_ylabel("Frequency (PHz)") - ax.set_title(title) - ax.set_xlim(-data.halfwidth / 1e-15, data.halfwidth / 1e-15) - if data.flim is not None: - ax.set_ylim(data.flim[0] / 1e15, data.flim[1] / 1e15) - return mpos, mneg - - -def draw_measured_log(ax, data: RetrievalPlotData) -> TraceArtists: - """Draw the measured trace, logarithmic (``tracedb`` dynamic range).""" - mpos, mneg = _draw_trace_image( - ax, data, data.measured, db=True, title="Measured (log)" - ) - return TraceArtists(mesh_pos=mpos, mesh_neg=mneg) - - -def draw_retrieved_log(ax, data: RetrievalPlotData) -> TraceArtists: - """Draw the retrieved trace, logarithmic.""" - mpos, mneg = _draw_trace_image( - ax, data, data.retrieved, db=True, title="Retrieved (log)" - ) - return TraceArtists(mesh_pos=mpos, mesh_neg=mneg) - - -def draw_measured_lin(ax, data: RetrievalPlotData) -> TraceArtists: - """Draw the measured trace, linear, on the scale shared with the retrieved.""" - mpos, mneg = _draw_trace_image( - ax, data, data.measured, db=False, title="Measured (lin)" - ) - return TraceArtists(mesh_pos=mpos, mesh_neg=mneg) - - -def draw_retrieved_lin(ax, data: RetrievalPlotData) -> TraceArtists: - """Draw the retrieved trace, linear, annotated with FROG error and shape.""" - mpos, mneg = _draw_trace_image( - ax, data, data.retrieved, db=False, title="Retrieved (lin)" - ) - label = ax.text( - 0.04, - 0.96, - _trace_label(data), - transform=ax.transAxes, - ha="left", - va="top", - color="black", - fontweight="bold", - fontsize="small", - ) - return TraceArtists(mesh_pos=mpos, mesh_neg=mneg, label=label) - - -def draw_temporal(ax, data: RetrievalPlotData) -> TemporalArtists: - """Draw the retrieved pulse: intensity/power, TL pulse, phase, truth overlay.""" - return _temporal_panel( - ax, - data.processed, - halfwidth=data.halfwidth, - power_scale=data.power_scale, - truth=data.truth, - ) - - -def draw_spectral(ax, data: RetrievalPlotData) -> SpectralArtists: - """Draw the spectrum: retrieved, measured, phase and fit, GDD/TOD, truth.""" - return _spectral_panel( - ax, - data.processed, - lam_min=data.lam_min, - lam_max=data.lam_max, - truth=data.truth, - axis=data.spectral_axis, - ) - - -def draw_convergence(ax, data: RetrievalPlotData) -> ConvergenceArtists: - """Draw the FROG error against iteration, with the solver's stage handovers.""" - return plot_convergence( - ax, data.processed.errors, boundaries=data.result.stage_boundaries - ) - - -def draw_residual(ax, data: RetrievalPlotData) -> ResidualArtists: - """Draw the measured − retrieved residual; leave it empty if there is none.""" - pr = data.processed - if pr.residual is None: - return ResidualArtists(mesh=None) - mesh = plot_residual( - ax, - data.result.omega, - data.result.delays, - pr.residual, - data.result.omega0, - halfwidth=data.halfwidth, - flim=data.flim, - ) - return ResidualArtists(mesh=mesh) - - -def _filter_factor(result: RetrievalResult) -> NDArray[np.float64]: - """Return the per-frequency Rω scale factor, peak-normalised, else flat.""" - mu = np.atleast_1d(np.asarray(result.mu, dtype=float)) - if mu.size == result.omega.size: - return mu / mu.max() - return np.ones_like(result.omega) - - -def draw_spectral_filter(ax, data: RetrievalPlotData) -> LineArtists: - """Draw the per-frequency scale factor (Rω filter) against wavelength.""" - # A centred retrieval grid can cross zero frequency, where the wavelength - # axis has a pole: harmless for the plot, so the divide warning is silenced. - with np.errstate(divide="ignore", invalid="ignore"): - lam_nm = wlfreq(data.result.omega + data.result.omega0) / 1e-9 - (line,) = ax.plot(lam_nm, _filter_factor(data.result), c="C2") - ax.set_xlabel("Wavelength (nm)") - ax.set_ylabel("Filter factor") - ax.set_title("Spectral filter") - ax.set_xlim(data.lam_min / 1e-9, data.lam_max / 1e-9) - return LineArtists(line=line) - - -def draw_freq_marginal(ax, data: RetrievalPlotData) -> MarginalArtists: - """Draw the frequency marginal of the retrieved and measured traces.""" - pr = data.processed - artists = plot_marginal( - ax, - data.result.omega + data.result.omega0, - pr.omega_marg_retr, - pr.omega_marg_meas, - xlabel="Frequency (PHz)", - scale=_TWOPI_PHZ, - ) - ax.set_title("Frequency marginal") - return artists - - -def draw_delay_marginal(ax, data: RetrievalPlotData) -> MarginalArtists: - """Draw the delay marginal of the retrieved and measured traces.""" - pr = data.processed - artists = plot_marginal( - ax, - data.result.delays, - pr.tau_marg_retr, - pr.tau_marg_meas, - xlabel="Delay (fs)", - scale=1e-15, - halfwidth=data.halfwidth, - ) - ax.set_title("Delay marginal") - return artists - - -def draw_spectrogram(ax, data: RetrievalPlotData) -> SpectrogramArtists: - """Draw the Gabor spectrogram of the retrieved pulse (computed once, cached).""" - tc, lam, S = data.spectrogram - mesh = plot_spectrogram( - ax, - tc, - lam, - S, - lam_min=data.lam_min, - lam_max=data.lam_max, - halfwidth=data.halfwidth, - # white at zero, like the trace panels and the dispersion stage's own - # spectrogram — this panel was the only one left on plain viridis. - cmap=data.cmap_pos, - ) - return SpectrogramArtists(mesh=mesh) - - -# --------------------------------------------------------------------------- -# In-place updates -# --------------------------------------------------------------------------- -# Each update_(artists, data) pushes a new result onto the artists its -# draw_ twin created: set_data / set_array / set_text and explicit limits, -# never new artists. The invariant is update(draw(d1), d2) ≡ draw(d2), which the -# tests check panel by panel. Whether an update is possible at all — same grid -# shapes, same optional artists — is decided by _update_signature. - - -def _axes_of(artist) -> Axes: - """Return the axes an artist is drawn on (an updated artist always has one).""" - ax = artist.axes - if ax is None: - raise RuntimeError("the artist is not on any axes") - return ax - - -def _update_trace(artists: TraceArtists, data: RetrievalPlotData, C, *, db: bool): - mag, neg, vmin, vhi = _signed_magnitude( - C, db=db, tracedb=data.tracedb, vmax=None if db else data.lin_vmax - ) - artists.mesh_pos.set_array(np.ma.masked_where(neg, mag)) - artists.mesh_pos.set_clim(vmin, vhi) # a shared colorbar follows the norm - if artists.mesh_neg is not None: - artists.mesh_neg.set_array(np.ma.masked_where(~neg, mag)) - artists.mesh_neg.set_clim(vmin, vhi) - - -def update_measured_log(artists: TraceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_measured_log`'s artists in place.""" - _update_trace(artists, data, data.measured, db=True) - - -def update_retrieved_log(artists: TraceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_retrieved_log`'s artists in place.""" - _update_trace(artists, data, data.retrieved, db=True) - - -def update_measured_lin(artists: TraceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_measured_lin`'s artists in place.""" - _update_trace(artists, data, data.measured, db=False) - - -def update_retrieved_lin(artists: TraceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_retrieved_lin`'s artists in place.""" - _update_trace(artists, data, data.retrieved, db=False) - if artists.label is not None: - artists.label.set_text(_trace_label(data)) - - -def update_temporal(artists: TemporalArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_temporal`'s artists in place.""" - pr = data.processed - t = pr.t_over / 1e-15 - retr, tl, r_label, tl_label, ylabel, power_scale = _temporal_curves( - pr, data.power_scale - ) - artists.line_retr.set_data(t, retr) - artists.line_tl.set_data(pr.t_tl / 1e-15, tl) - artists.legend_retr.set_text(r_label) - artists.legend_tl.set_text(tl_label) - ax = _axes_of(artists.line_retr) - ax.set_ylabel(ylabel) - truth_curve = None - if artists.line_truth is not None and data.truth is not None: - truth_curve = _truth_intensity(data.truth, pr.energy, power_scale) - artists.line_truth.set_ydata(truth_curve) - ax.set_ylim(*_intensity_ylim(retr, tl, truth_curve)) - artists.line_phase.set_data(t[pr.mask_t], pr.phi_t[pr.mask_t]) - truth_phase = None # static within a run: the truth does not change - if artists.line_truth_phase is not None: - truth_phase = np.asarray(artists.line_truth_phase.get_ydata(), dtype=float) - limits = _phase_ylim(pr.phi_t[pr.mask_t], truth_phase) - if limits is not None: - artists.ax_phase.set_ylim(*limits) - - -def update_spectral(artists: SpectralArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_spectral`'s artists in place.""" - pr = data.processed - x, retrieved, measured, _xlabel = _spectral_curves(pr, data.spectral_axis) - artists.line_retr.set_data(x, retrieved) - if artists.line_meas is not None and measured is not None: - artists.line_meas.set_data(x, measured) - artists.line_phase.set_data(x[pr.mask_w], pr.phi_w[pr.mask_w]) - artists.line_phase_fit.set_data(x[pr.mask_w], pr.phi_w_fit[pr.mask_w]) - # The truth phase is static but its mask (where the spectrum is bright) moves. - tphi = _truth_spectral_phase(pr.wavelength, data.truth) - if artists.line_truth_phase is not None and tphi is not None: - artists.line_truth_phase.set_data(x[pr.mask_w], tphi[pr.mask_w]) - limits = _phase_ylim(pr.phi_w[pr.mask_w], None if tphi is None else tphi[pr.mask_w]) - if limits is not None: - artists.ax_phase.set_ylim(*limits) - artists.text_dispersion.set_text(_dispersion_text(pr)) - edge, edge_color = _edge_text(pr) - artists.text_edge.set_text(edge) - artists.text_edge.set_color(edge_color) - - -def update_convergence_curve(artists: ConvergenceArtists, errors: ArrayLike) -> None: - """Grow the error curve of :func:`plot_convergence` in place and rescale.""" - errors = np.asarray(errors, dtype=float) - artists.line.set_data(np.arange(1, errors.size + 1), errors) - ax = _axes_of(artists.line) - ax.relim() # autoscaling is still on here: plot_convergence sets no limits - ax.autoscale_view() - - -def update_convergence(artists: ConvergenceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_convergence`'s artists in place.""" - update_convergence_curve(artists, data.processed.errors) - - -def update_residual(artists: ResidualArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_residual`'s artists in place.""" - resid = data.processed.residual - if artists.mesh is None or resid is None: - return - mr = float(np.max(np.abs(resid))) or 1.0 - artists.mesh.set_array(resid) - artists.mesh.set_clim(-mr, mr) - - -def update_spectral_filter(artists: LineArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_spectral_filter`'s artists in place (x is the fixed grid).""" - artists.line.set_ydata(_filter_factor(data.result)) - - -def update_freq_marginal(artists: MarginalArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_freq_marginal`'s artists in place.""" - pr = data.processed - artists.line_retr.set_ydata(pr.omega_marg_retr) - if artists.line_meas is not None and pr.omega_marg_meas is not None: - artists.line_meas.set_ydata(pr.omega_marg_meas) - - -def update_delay_marginal(artists: MarginalArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_delay_marginal`'s artists in place.""" - pr = data.processed - artists.line_retr.set_ydata(pr.tau_marg_retr) - if artists.line_meas is not None and pr.tau_marg_meas is not None: - artists.line_meas.set_ydata(pr.tau_marg_meas) - - -def update_spectrogram(artists: SpectrogramArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_spectrogram`'s artists in place. - - Raises - ------ - ValueError - If the spectrogram's shape differs from the drawn one (a different grid): - the panel has to be redrawn, not updated. - """ - _tc, _lam, S = data.spectrogram - array = artists.mesh.get_array() - drawn = () if array is None else tuple(np.shape(array)) - if drawn != S.shape: - raise ValueError(f"spectrogram shape changed from {drawn} to {S.shape}") - artists.mesh.set_array(S) - artists.mesh.autoscale() # what the first draw did implicitly - - -def _update_signature(data: RetrievalPlotData) -> tuple: - """Everything that decides which artists exist and the mesh geometry. - - Two bundles with equal signatures differ only in values, so a panel drawn for - one can be updated in place for the other. Components: the trace, grid and - oversampled-axis shapes; whether the traces hold negatives (the second - colormap); which optional curves exist (residual, measured spectrum and - marginals, absolute power, the truth and its phases); how many stage-handover - rules are visible; and the fixed axis bounds. - """ - pr = data.processed - result = data.result - truth = data.truth - n_rules = sum(1 for edge in result.stage_boundaries if 0 < edge < len(pr.errors)) - return ( - data.retrieved.shape, - data.measured.shape, - result.omega.size, - result.delays.size, - pr.t_over.size, - pr.t_tl.size, - pr.wavelength.size, - bool((data.measured < 0).any()), - bool((data.retrieved < 0).any()), - pr.residual is not None, - pr.Iw_meas is not None, - pr.omega_marg_meas is not None, - pr.tau_marg_meas is not None, - pr.peak_power is not None, - truth is not None, - truth is not None and truth.phi_t is not None, - truth is not None and truth.lam is not None and truth.Iw is not None, - truth is not None and truth.lam is not None and truth.phi_w is not None, - n_rules, - data.lam_min, - data.lam_max, - data.flim, - data.halfwidth, - data.tracedb, - data.spectral_axis, - ) - - -type PanelDraw = Callable[[Axes, RetrievalPlotData], PanelArtists] -# Any: each update_ takes its own artists dataclass, and a registry entry -# typed with the union would reject them (parameters are contravariant). The draw -# side keeps the precise union because a return type is covariant. -type PanelUpdate = Callable[[Any, RetrievalPlotData], None] - - -@dataclass(frozen=True) -class PanelSpec: - """One retrieval panel: registry key, axes title, draw and in-place update.""" - - key: str - title: str - draw: PanelDraw - update: PanelUpdate - - -@dataclass(frozen=True) -class PageSpec: - """A figure's worth of panels laid out as a grid of registry keys. - - Attributes - ---------- - key, title : str - Identifier and human-readable name (the GUI's tab label). - mosaic : tuple of tuple of str - Rows of panel keys, as - :external+matplotlib:meth:`~matplotlib.figure.Figure.subplot_mosaic` - takes them. - """ - - key: str - title: str - mosaic: tuple[tuple[str, ...], ...] - - @property - def keys(self) -> tuple[str, ...]: - """The page's panel keys in reading order.""" - return tuple(key for row in self.mosaic for key in row) - - -@dataclass(frozen=True) -class ColorbarSpec: - """A colorbar shared by the listed panels (drawn only when all are present). - - A positive colorbar is always drawn; a second, ``neg_label``-ed one appears - when any of the panels holds negative samples (see :func:`signed_pcolormesh`). - """ - - panels: tuple[str, ...] - shrink: float = 0.6 - neg_label: str = "neg |·|" - - -#: The twelve retrieval panels, keyed, in the overview's reading order. -RETRIEVAL_PANELS: Mapping[str, PanelSpec] = { - spec.key: spec - for spec in ( - PanelSpec( - "measured_log", "Measured (log)", draw_measured_log, update_measured_log - ), - PanelSpec( - "retrieved_log", "Retrieved (log)", draw_retrieved_log, update_retrieved_log - ), - PanelSpec("temporal", "Retrieved pulse", draw_temporal, update_temporal), - PanelSpec( - "delay_marginal", - "Delay marginal", - draw_delay_marginal, - update_delay_marginal, - ), - PanelSpec( - "measured_lin", "Measured (lin)", draw_measured_lin, update_measured_lin - ), - PanelSpec( - "retrieved_lin", "Retrieved (lin)", draw_retrieved_lin, update_retrieved_lin - ), - PanelSpec("spectral", "Spectrum", draw_spectral, update_spectral), - PanelSpec( - "freq_marginal", - "Frequency marginal", - draw_freq_marginal, - update_freq_marginal, - ), - PanelSpec("convergence", "Convergence", draw_convergence, update_convergence), - PanelSpec("residual", "Residuals", draw_residual, update_residual), - PanelSpec( - "spectral_filter", - "Spectral filter", - draw_spectral_filter, - update_spectral_filter, - ), - PanelSpec("spectrogram", "Spectrogram", draw_spectrogram, update_spectrogram), - ) -} - -#: Colorbars: one per trace pair (they share a scale) and one for the residual. -RETRIEVAL_COLORBARS: tuple[ColorbarSpec, ...] = ( - ColorbarSpec(("measured_log", "retrieved_log")), - ColorbarSpec(("measured_lin", "retrieved_lin")), - ColorbarSpec(("residual",)), -) - -#: The three 2×2 pages the GUI shows as tabs. -RETRIEVAL_PAGES: Mapping[str, PageSpec] = { - page.key: page - for page in ( - PageSpec( - "traces", - "Traces", - (("measured_log", "retrieved_log"), ("measured_lin", "retrieved_lin")), - ), - # Each marginal sits under the domain it belongs to: delay under the - # time-domain pulse, frequency under the spectrum. - PageSpec( - "pulse", - "Pulse", - (("temporal", "spectral"), ("delay_marginal", "freq_marginal")), - ), - PageSpec( - "diagnostics", - "Diagnostics", - (("convergence", "residual"), ("spectral_filter", "spectrogram")), - ), - ) -} - -#: The 3×4 overview :func:`plot_retrieval` draws: traces left, pulse right (the -#: delay marginal beside the time-domain pulse, the frequency marginal beside the -#: spectrum), diagnostics along the bottom. -RETRIEVAL_OVERVIEW = PageSpec( - "overview", - "Retrieval overview", - ( - ("measured_log", "retrieved_log", "temporal", "delay_marginal"), - ("measured_lin", "retrieved_lin", "spectral", "freq_marginal"), - ("convergence", "residual", "spectral_filter", "spectrogram"), - ), -) - - -def _mappables(artists: PanelArtists) -> tuple[QuadMesh | None, QuadMesh | None]: - """Return the ``(positive, negative)`` images a colorbar can attach to.""" - if isinstance(artists, TraceArtists): - return artists.mesh_pos, artists.mesh_neg - if isinstance(artists, ResidualArtists): - return artists.mesh, None - return None, None - - -def _draw_colorbars( - fig, axd: Mapping[str, Axes], artists: Mapping[str, PanelArtists] -) -> None: - """Add every :data:`RETRIEVAL_COLORBARS` entry whose panels are all in ``axd``.""" - for spec in RETRIEVAL_COLORBARS: - if not all(key in axd for key in spec.panels): - continue - axes = [axd[key] for key in spec.panels] - pairs = [_mappables(artists[key]) for key in spec.panels] - pos = next((p for p, _ in pairs if p is not None), None) - if pos is None: - continue - # Both panels of a pair share vmin/vmax and cmap, so either can seed the bar. - fig.colorbar(pos, ax=axes, shrink=spec.shrink) - neg = next((n for _, n in pairs if n is not None), None) - if neg is not None: - fig.colorbar(neg, ax=axes, shrink=spec.shrink, label=spec.neg_label) - - -def _draw_panels( - fig, page: PageSpec, data: RetrievalPlotData -) -> tuple[dict[str, Axes], dict[str, PanelArtists]]: - """Lay ``page``'s mosaic out in ``fig``, draw every panel and its colourbars.""" - axd = fig.subplot_mosaic([list(row) for row in page.mosaic]) - artists = {key: RETRIEVAL_PANELS[key].draw(ax, data) for key, ax in axd.items()} - _draw_colorbars(fig, axd, artists) - return axd, artists - - -class RetrievalPageView: - """Maintain a page of retrieval plots for repeated GUI updates. - - The live preview hands the Retrieve stage a new result a few times a second. - Clearing a figure and rebuilding a page's axes, colourbars and legends for - each one costs a full constrained-layout solve and lets legends jump between - frames; this view draws the page once and then pushes later results onto the - existing artists when :meth:`can_update` accepts the new data, as - :class:`FrogFilterView` does for the preprocessing figure. It redraws when - the grid, optional curves or panel structure change. - - Parameters - ---------- - page : PageSpec - Panels and their layout. - - Examples - -------- - >>> view = RetrievalPageView(RETRIEVAL_PAGES["pulse"]) # doctest: +SKIP - >>> view.draw(fig, data1) # doctest: +SKIP - >>> if view.can_update(data2): # doctest: +SKIP - ... view.update(data2) - ... else: - ... view.draw(fig, data2) - """ - - def __init__(self, page: PageSpec): - self.page = page - self.fig: Figure | None = None - self.axd: dict[str, Axes] = {} - self.artists: dict[str, PanelArtists] = {} - self._signature: tuple | None = None - - @property - def attached(self) -> bool: - """Whether the view has drawn into a figure.""" - return self.fig is not None - - def draw(self, fig: Figure, data: RetrievalPlotData) -> None: - """Clear ``fig`` and draw the page afresh for ``data``. - - Constrained layout is solved on the first real draw and then frozen (the - engine is swapped for matplotlib's placeholder), so later in-place updates - neither re-solve it nor nudge the panels as tick labels change width. - """ - _prepare_fig(fig, None) - self.fig = fig - self.axd, self.artists = _draw_panels(fig, self.page, data) - self._signature = _update_signature(data) - canvas = fig.canvas - - def freeze(_event) -> None: - canvas.mpl_disconnect(cid) - if self.fig is fig: - fig.set_layout_engine("none") - - cid = canvas.mpl_connect("draw_event", freeze) - - def can_update(self, data: RetrievalPlotData) -> bool: - """Whether ``data`` differs from the drawn result in values only.""" - return self.attached and _update_signature(data) == self._signature - - def update(self, data: RetrievalPlotData) -> None: - """Push ``data`` onto the drawn artists. - - Raises - ------ - RuntimeError - If nothing has been drawn yet. - ValueError - If :meth:`can_update` is false (different grid or optional artists); - :meth:`draw` it instead. - """ - if not self.attached: - raise RuntimeError("draw(fig, data) before update(data)") - if not self.can_update(data): - raise ValueError("the result's shape or contents changed: draw() it") - for key, artists in self.artists.items(): - RETRIEVAL_PANELS[key].update(artists, data) - - def reset(self) -> None: - """Forget the figure (someone else cleared it); the next call must draw.""" - self.fig = None - self.axd = {} - self.artists = {} - self._signature = None - - -def plot_retrieval_page( - data: RetrievalPlotData, - page: PageSpec, - *, - figsize: tuple[float, float] = (8.5, 6.3), - fig: Figure | None = None, -) -> Figure: - """Draw one page of retrieval panels (a :data:`RETRIEVAL_PAGES` entry). - - The default ``figsize`` gives a 2×2 page the same per-panel size as the - overview's default 17.0×9.45 in. - - Parameters - ---------- - data : RetrievalPlotData - From :func:`retrieval_plot_data`. - page : PageSpec - Which panels, in which grid; :data:`RETRIEVAL_OVERVIEW` draws all twelve. - figsize : tuple of float - Size (in) of a new figure; ignored when ``fig`` is given. - fig : Figure, optional - Existing figure to clear and draw into (a GUI canvas). - - Examples - -------- - >>> data = retrieval_plot_data(result, measured=trace) # doctest: +SKIP - >>> fig = plot_retrieval_page(data, RETRIEVAL_PAGES["pulse"]) # doctest: +SKIP - """ - fig = _prepare_fig(fig, figsize) - _draw_panels(fig, page, data) - return fig - - -def plot_retrieval( - result: RetrievalResult, - *, - measured: ArrayLike | None = None, - Iomega_meas: ArrayLike | None = None, - lam_min: float | None = None, - lam_max: float | None = None, - flim: tuple[float, float] | None = None, - tracedb: float = 30.0, - cmap: str = "viridis", - truth: TruthPulse | None = None, - energy: float | None = None, - figsize=(17.0, 9.45), - fig: Figure | None = None, - processed: ProcessedResult | None = None, - spectral_axis: SpectralAxis = "frequency", -) -> Figure: - """12-panel retrieval summary: the standard at-a-glance quality check. - - The panels are :data:`RETRIEVAL_PANELS` in the :data:`RETRIEVAL_OVERVIEW` - layout — measured/retrieved traces (log and linear) on the left, the pulse, - spectrum and marginals on the right, convergence, residual, spectral filter - and spectrogram along the bottom. :func:`plot_retrieval_page` draws a subset. - - ``lam_min``/``lam_max`` (m) bound the **wavelength** axes — the spectrum and - spectrogram panels. ``flim`` bounds the **frequency** axis of the trace and - residual panels, as ``(f_min, f_max)`` in Hz; the default shows the whole - retrieval grid. The two are separate because the trace sits at the *signal* - frequency, which for SHG is twice the pulse's. Reach for ``flim`` when the - retrieval grid is much wider than the signal — typical when retrieving - directly on a synthesis grid rather than on one cropped by - :func:`~croak.preprocess.load_and_clean` — where the trace would otherwise be - a thin stripe in a mostly empty axis. - - ``energy`` is an optional measured pulse energy (J); when given, the temporal - panel is shown in absolute power (see :func:`~croak.processing.process_result`). - - ``processed`` is an optional, already-computed - :class:`~croak.processing.ProcessedResult` **for this same result**: pass it when - the caller needs the processed quantities too (the GUI shows them in a table - beside the figure) so they are not derived twice. It must have been built with - the same ``measured``/``Iomega_meas``/``energy``, which are then used only for - the trace panels; a mismatched one raises :class:`ValueError`. - - ``spectral_axis`` draws the spectrum panel against ``"frequency"`` (PHz, the - default) or ``"wavelength"`` (nm); see :func:`plot_spectral`. - - Raises - ------ - ValueError - If ``result`` carries no trace, or ``processed`` belongs to another result. - """ - data = retrieval_plot_data( - result, - measured=measured, - Iomega_meas=Iomega_meas, - lam_min=lam_min, - lam_max=lam_max, - flim=flim, - tracedb=tracedb, - cmap=cmap, - truth=truth, - energy=energy, - processed=processed, - spectral_axis=spectral_axis, - ) - return plot_retrieval_page(data, RETRIEVAL_OVERVIEW, figsize=figsize, fig=fig) - - -def filter_view_vmax(td) -> float: - """Return the shared positive colour scale for the six-panel filter view. - - Take the largest **positive** sample across the measured, filtered and - regridded traces, restricting the two wavelength-domain arrays to the - measurement window ``td.lamm_lims`` when set. - - Both restrictions matter on real data. An absolute-calibration curve - amplifies the background-subtracted noise far outside the signal band, so - scaling by ``max|·|`` lets a large *negative* excursion set the *positive* - scale: on one of our measured deep-UV traces, whose signal peaks at ``+1`` - but dips to ``-5.7`` at 1097 nm, every linear panel rendered its signal at - 18 % of the colour range. - - The three panels share this one number so the row's single colourbar means - something; each array is peak-normalised upstream, so they are comparable. - If no positive sample remains, fall back to the largest absolute value, - then 1.0, so the returned scale is always positive. - """ - - def _positive_max(arr, lam=None) -> float: - a = np.asarray(arr, dtype=float) - if lam is not None and td.lamm_lims is not None: - lo, hi = td.lamm_lims - band = (np.asarray(lam, dtype=float) >= lo) & ( - np.asarray(lam, dtype=float) <= hi - ) - if band.any(): - a = a[band] - hi = float(np.nanmax(a)) if a.size else 0.0 - return hi if hi > 0.0 else 0.0 - - vmax = max( - _positive_max(td.Ifrog_meas, td.lam_frog), - _positive_max(td.Ifrog_filt, td.lam_frog), - _positive_max(td.trace), - ) - if vmax > 0.0: - return vmax - # Degenerate (all-negative or all-zero) trace: keep a usable, positive scale. - fallback = max( - float(np.nanmax(np.abs(td.Ifrog_meas))), - float(np.nanmax(np.abs(td.Ifrog_filt))), - float(np.nanmax(np.abs(td.trace))), - ) - return fallback if fallback > 0.0 else 1.0 - - -def plot_frog_filter( - td, - *, - tracedb: float = 30.0, - cmap: str = "viridis", - figsize=(9.45, 4.72), - fig: Figure | None = None, -) -> Figure: - """6-panel measured/filtered (wavelength-domain) + regridded (frequency-domain). - - Positive values use the viridis (white-low) map; negative values — kept - through filtering rather than clamped — are drawn by magnitude on a - white→red map so they are visible and not mistaken for signal. Each row - (the dB row and the linear row) carries one positive + one negative - colorbar on the right-hand side. - """ - fig = _prepare_fig(fig, figsize) - axd = fig.subplot_mosaic("abe\ncdf") - cm = cmap_white(cmap) - # The linear row shares a single scale so its one colorbar is meaningful. - lin_vmax = filter_view_vmax(td) - - def _panel(ax, x, y, C, *, db, title, xlabel): - mpos, mneg = signed_pcolormesh( - ax, - x, - y, - C, - db=db, - tracedb=tracedb, - cmap_pos=cm, - vmax=None if db else lin_vmax, - ) - ax.set_xlabel(xlabel) - ax.set_ylabel("Delay (fs)") - ax.set_title(title) - return mpos, mneg - - def _row_cbars(axes, mpos, negs): - """One positive + (if any) one negative colorbar at the row's right.""" - fig.colorbar(mpos, ax=axes, shrink=0.5) - mneg = next((m for m in negs if m is not None), None) - if mneg is not None: - fig.colorbar(mneg, ax=axes, shrink=0.5, label="neg |·|") - - lam_nm = td.lam_frog / 1e-9 - freq = (td.omega + td.omega0_trace) / _TWOPI_PHZ - WL = "Wavelength (nm)" - HZ = "Frequency (PHz)" - - # top row: dB (log) — measured, filtered, regridded - pa, na = _panel( - axd["a"], - lam_nm, - td.tau_meas / 1e-15, - td.Ifrog_meas.T, - db=True, - title="Measured (dB)", - xlabel=WL, - ) - _pb, nb = _panel( - axd["b"], - lam_nm, - td.tau_filt / 1e-15, - td.Ifrog_filt.T, - db=True, - title="Filtered (dB)", - xlabel=WL, - ) - _pe, ne = _panel( - axd["e"], - freq, - td.delays / 1e-15, - td.trace.T, - db=True, - title="Regridded (dB)", - xlabel=HZ, - ) - _row_cbars([axd["a"], axd["b"], axd["e"]], pa, (nb, ne, na)) - - # bottom row: linear — measured, filtered, regridded - pc, nc = _panel( - axd["c"], - lam_nm, - td.tau_meas / 1e-15, - td.Ifrog_meas.T, - db=False, - title="Measured (lin)", - xlabel=WL, - ) - _pd, nd = _panel( - axd["d"], - lam_nm, - td.tau_filt / 1e-15, - td.Ifrog_filt.T, - db=False, - title="Filtered (lin)", - xlabel=WL, - ) - _pf, nf = _panel( - axd["f"], - freq, - td.delays / 1e-15, - td.trace.T, - db=False, - title="Regridded (lin)", - xlabel=HZ, - ) - _row_cbars([axd["c"], axd["d"], axd["f"]], pc, (nd, nf, nc)) - return fig - - -def _decimate_for_display(C, x, y, *, cap=1000): - """Stride a ``(ny, nx)`` image and its axes down to at most ``cap`` per side. - - The filter figure panels are only a few hundred screen pixels wide, so - rasterising a multi-thousand-sample trace is wasted work. Plain strided - subsampling is enough for a diagnostic preview (no anti-alias needed — the - physics-correct arrays are kept in ``td``; this only thins what is drawn). - """ - ny, nx = C.shape - sy = max(ny // cap, 1) - sx = max(nx // cap, 1) - if sy == 1 and sx == 1: - return C, x, y - return C[::sy, ::sx], x[::sx], y[::sy] - - -def _resample_uniform_x(x, C): - """Resample ``C`` (``(ny, nx)``) along axis 1 onto a uniform ``x`` grid. - - :external+matplotlib:class:`~matplotlib.image.AxesImage` (``imshow``) assumes evenly - spaced pixels and is positioned by a single linear ``extent``. A *wavelength* axis - derived from a uniform **frequency** grid (the simulated-trace case) is strongly - non-uniform, so a feature would otherwise be painted at the wrong wavelength (e.g. a - 260 nm signal drawn near 480 nm over a 150–800 nm span). Linearly interpolating each - delay row onto an evenly spaced ``x`` restores the correct positions; an - already-uniform axis (the frequency panels, or a uniform-λ spectrometer trace) is - returned unchanged. ``x`` must be ascending. - """ - x = np.asarray(x, dtype=float) - if x.size < 3: - return x, C - d = np.diff(x) - if np.allclose(d, d[0], rtol=1e-3, atol=0.0): - return x, C # already uniform — no interpolation needed - xu = np.linspace(x[0], x[-1], x.size) - Cu = np.empty_like(C) - for i in range(C.shape[0]): - Cu[i] = np.interp(xu, x, C[i]) - return xu, Cu - - -class FrogFilterView: - """Maintain a preprocessing figure for repeated GUI updates. - - The artist-reusing twin of :func:`plot_frog_filter`, which clears and - rebuilds the whole figure (6 panels × up to 2 ``pcolormesh`` + 4 - colourbars) on every call: ~150–400 ms, the dominant cost of the GUI - preprocessing redraw. Call :meth:`attach` once to create the panels, images - and colourbars, then :meth:`update` to push new data via - ``set_data``/``set_extent``/``set_clim`` onto the persistent images, which - benchmarks ~4× faster. Positive and negative samples use the same - signed-magnitude convention as :func:`signed_pcolormesh`. Non-uniform - wavelength arrays are interpolated onto a uniform display grid; the - underlying :class:`croak.preprocess.TraceData` arrays are unchanged. - """ - - _PANELS = ( - # key, title, xlabel-kind ("wl"/"hz"), db - ("a", "Measured (dB)", "wl", True), - ("b", "Filtered (dB)", "wl", True), - ("e", "Regridded (dB)", "hz", True), - ("c", "Measured (lin)", "wl", False), - ("d", "Filtered (lin)", "wl", False), - ("f", "Regridded (lin)", "hz", False), - ) - - def __init__( - self, *, tracedb: float = 30.0, cmap: str = "viridis", cap: int = 1000 - ): - self.tracedb = tracedb - self.cap = cap - self._cmap_pos = cmap_white(cmap) - self._cmap_pos.set_bad(alpha=0.0) # masked (negative) pixels stay clear - self._cmap_neg = _white_red() - self._cmap_neg.set_bad(alpha=0.0) - self.fig = None - self.axd = None - self.images: dict[str, tuple] = {} - - @property - def attached(self) -> bool: - """Whether a figure is currently attached to this view.""" - return self.fig is not None - - def attach(self, fig: Figure) -> None: - """Build the mosaic, images and colourbars once into ``fig``.""" - self.fig = fig - fig.clear() - with contextlib.suppress(Exception): - fig.set_layout_engine("constrained") - self.axd = fig.subplot_mosaic("abe\ncdf") - empty = np.zeros((1, 1)) - for key, title, kind, _db in self._PANELS: - ax = self.axd[key] - pos = ax.imshow( - empty, - origin="lower", - aspect="auto", - cmap=self._cmap_pos, - interpolation="nearest", - ) - neg = ax.imshow( - empty, - origin="lower", - aspect="auto", - cmap=self._cmap_neg, - interpolation="nearest", - ) - self.images[key] = (pos, neg) - ax.set_title(title) - ax.set_xlabel("Wavelength (nm)" if kind == "wl" else "Frequency (PHz)") - ax.set_ylabel("Delay (fs)") - # One positive + one negative colorbar per row (dB row a/b/e, lin row c/d/f). - self._cb_top = fig.colorbar( - self.images["a"][0], ax=[self.axd[k] for k in "abe"], shrink=0.5 - ) - self._cb_top_neg = fig.colorbar( - self.images["a"][1], - ax=[self.axd[k] for k in "abe"], - shrink=0.5, - label="neg |·|", - ) - self._cb_bot = fig.colorbar( - self.images["c"][0], ax=[self.axd[k] for k in "cdf"], shrink=0.5 - ) - self._cb_bot_neg = fig.colorbar( - self.images["c"][1], - ax=[self.axd[k] for k in "cdf"], - shrink=0.5, - label="neg |·|", - ) - - def _set_panel(self, key, x, y, C, *, db, vmax=None): - if self.axd is None: - raise RuntimeError("live plot is not attached to a figure") - pos_im, neg_im = self.images[key] - C = np.asarray(C, dtype=float) - C, x, y = _decimate_for_display(C, np.asarray(x), np.asarray(y), cap=self.cap) - # imshow paints evenly spaced pixels; a wavelength axis built from a - # uniform frequency grid (simulated traces) is non-uniform, so resample - # onto an even axis to place features at the right wavelength. - x, C = _resample_uniform_x(x, C) - neg = C < 0 - if db: - mag = np.clip( - 10 * np.log10(np.maximum(np.abs(C), 1e-30)), -self.tracedb, 0.0 - ) - vmin, vhi = -self.tracedb, 0.0 - else: - mag = np.abs(C) - vmin = 0.0 - vhi = float(np.nanmax(mag)) if vmax is None else vmax - extent = (float(x[0]), float(x[-1]), float(y[0]), float(y[-1])) - for im, masked in ( - (pos_im, np.ma.masked_where(neg, mag)), - (neg_im, np.ma.masked_where(~neg, mag)), - ): - im.set_data(masked) - im.set_clim(vmin, vhi) - im.set_extent(extent) - ax = self.axd[key] - ax.set_xlim(extent[0], extent[1]) - ax.set_ylim(extent[2], extent[3]) - - def update(self, td) -> None: - """Push a new :class:`~croak.preprocess.TraceData` onto the existing artists.""" - if not self.attached: - raise RuntimeError("attach(fig) before update(td)") - lam_nm = td.lam_frog / 1e-9 - freq = (td.omega + td.omega0_trace) / _TWOPI_PHZ - lin_vmax = filter_view_vmax(td) - self._set_panel("a", lam_nm, td.tau_meas / 1e-15, td.Ifrog_meas.T, db=True) - self._set_panel("b", lam_nm, td.tau_filt / 1e-15, td.Ifrog_filt.T, db=True) - self._set_panel("e", freq, td.delays / 1e-15, td.trace.T, db=True) - self._set_panel( - "c", lam_nm, td.tau_meas / 1e-15, td.Ifrog_meas.T, db=False, vmax=lin_vmax - ) - self._set_panel( - "d", lam_nm, td.tau_filt / 1e-15, td.Ifrog_filt.T, db=False, vmax=lin_vmax - ) - self._set_panel( - "f", freq, td.delays / 1e-15, td.trace.T, db=False, vmax=lin_vmax - ) - # The linear row's scale changes with the data; refresh its colorbars. - self._cb_bot.update_normal(self.images["c"][0]) - self._cb_bot_neg.update_normal(self.images["c"][1]) - - -def plot_simulated_trace( - omega, - delays, - trace, - omega0_trace, - *, - tracedb: float = 40.0, - cmap: str = "viridis", - figsize=(7.87, 5.51), - fig: Figure | None = None, -) -> Figure: - """4-panel view of a simulated trace: dB, linear, and the two marginals.""" - trace = np.asarray(trace, dtype=float) - fig = _prepare_fig(fig, figsize) - axd = fig.subplot_mosaic("ab\ncd") - cm = cmap_white(cmap) - freq = (omega + omega0_trace) / _TWOPI_PHZ - - def _panel(ax, *, db, title): - mpos, mneg = signed_pcolormesh( - ax, delays / 1e-15, freq, trace, db=db, tracedb=tracedb, cmap_pos=cm - ) - ax.set_xlabel("Delay (fs)") - ax.set_ylabel("Frequency (PHz)") - ax.set_title(title) - return mpos, mneg - - ma, na = _panel(axd["a"], db=True, title="FROG (dB)") - mb, nb = _panel(axd["b"], db=False, title="FROG (lin)") - fig.colorbar(ma, ax=[axd["a"]], shrink=0.6) - if na is not None: - fig.colorbar(na, ax=[axd["a"]], shrink=0.6, label="neg |·|") - fig.colorbar(mb, ax=[axd["b"]], shrink=0.6) - if nb is not None: - fig.colorbar(nb, ax=[axd["b"]], shrink=0.6, label="neg |·|") - wmarg, tmarg = marginals(trace) - axd["c"].plot(delays / 1e-15, tmarg, c="C0") - axd["c"].set_xlabel("Delay (fs)") - axd["c"].set_ylabel("Marginal") - axd["c"].set_ylim(0, 1.05) - axd["d"].plot((omega + omega0_trace) / _TWOPI_PHZ, wmarg, c="C0") - axd["d"].set_xlabel("Frequency (PHz)") - axd["d"].set_ylabel("Marginal") - axd["d"].set_ylim(0, 1.05) - return fig - - -def _interaction_scale(name: str) -> float: - from .interactions import get_interaction - - return get_interaction(name).omega0_scale - - -# --------------------------------------------------------------------------- -# Uncertainty -# --------------------------------------------------------------------------- -def plot_fwhm_distribution( - ax, uresult: UncertaintyResult, *, truth: float | None = None -) -> None: - """Histogram of bootstrap FWHM samples with the point estimate and intervals. - - Parameters - ---------- - ax : matplotlib.axes.Axes - Axis to draw into. - uresult : UncertaintyResult - The bootstrap result (its ``samples`` are histogrammed). - truth : float, optional - A known true FWHM (s) to mark with a dashed line (synthetic tests). - """ - samples = np.asarray(uresult.samples, dtype=float) / 1e-15 - nbins = int(np.clip(samples.size // 5, 12, 40)) - ax.hist(samples, bins=nbins, color="C0", alpha=0.7, edgecolor="white", lw=0.4) - lo95, hi95 = uresult.interval_95 - lo68, hi68 = uresult.interval_68 - ax.axvspan(lo95 / 1e-15, hi95 / 1e-15, color="C0", alpha=0.10, label="95 %") - ax.axvspan(lo68 / 1e-15, hi68 / 1e-15, color="C0", alpha=0.20, label="68 %") - ax.axvline(uresult.point_estimate / 1e-15, c="C3", lw=1.5, label="estimate") - if truth is not None: - ax.axvline(truth / 1e-15, c="0.35", ls="--", lw=1.2, label="truth") - ax.set_xlabel("FWHM (fs)") - ax.set_ylabel("Replicates") - ax.set_title( - f"{uresult.point_estimate / 1e-15:.2f} ± {uresult.plus_minus / 1e-15:.2f} fs" - f" (B={uresult.n_converged})" - ) - ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") - - -def plot_intensity_band(ax, uresult: UncertaintyResult) -> None: - """Plot the median temporal intensity and pointwise 68 %/95 % resampling bands. - - Requires the bootstrap to have been run with ``collect_profiles=True``. The profiles - are peak-aligned (translation gauge removed by ``shiftnorm``), so the band is the - spread of the aligned intensity envelope across replicates. The bands are - pointwise, not simultaneous bands for the whole curve. - """ - if uresult.profiles is None or uresult.t_profile is None: - raise ValueError( - "uresult has no profiles; run the bootstrap with collect_profiles=True" - ) - t = np.asarray(uresult.t_profile, dtype=float) / 1e-15 - profiles = np.asarray(uresult.profiles, dtype=float) - med = np.median(profiles, axis=0) - lo68, hi68 = np.percentile(profiles, [16.0, 84.0], axis=0) - lo95, hi95 = np.percentile(profiles, [2.5, 97.5], axis=0) - ax.fill_between(t, lo95, hi95, color="C0", alpha=0.15, label="95 %") - ax.fill_between(t, lo68, hi68, color="C0", alpha=0.30, label="68 %") - ax.plot(t, med, c="C0", lw=1.5, label="median") - ax.set_xlabel("Time (fs)") - ax.set_ylabel("Intensity (a.u.)") - ax.set_ylim(bottom=0.0) - ax.set_title("Temporal intensity") - # zoom to where the envelope carries signal - support = t[med > 0.01 * med.max()] - if support.size: - pad = 0.2 * (support.max() - support.min() + 1.0) - ax.set_xlim(support.min() - pad, support.max() + pad) - ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") - - -def plot_uncertainty( - uresult: UncertaintyResult, - *, - truth: float | None = None, - figsize=(11.0, 4.2), - fig: Figure | None = None, -) -> Figure: - """Composite uncertainty figure: FWHM histogram + a confidence band if present. - - Parameters - ---------- - uresult : UncertaintyResult - The bootstrap result. - truth : float, optional - Known true FWHM (s) for the histogram (synthetic tests). - figsize : tuple, optional - Figure size. - fig : matplotlib.figure.Figure, optional - Existing figure to draw into (cleared first), e.g. a Qt canvas figure. - - Returns - ------- - matplotlib.figure.Figure - """ - # A thickness-systematic result carries per-draw thicknesses; show the dedicated - # sensitivity figure so the GUI canvas path needs no special-casing. - if getattr(uresult, "thicknesses", None) is not None: - return plot_thickness_sensitivity(uresult, truth=truth, fig=fig) - fig = _prepare_fig(fig, figsize) - if uresult.profiles is not None and uresult.t_profile is not None: - ax_h, ax_b = fig.subplots(1, 2) - plot_fwhm_distribution(ax_h, uresult, truth=truth) - plot_intensity_band(ax_b, uresult) - else: - ax_h = fig.subplots(1, 1) - plot_fwhm_distribution(ax_h, uresult, truth=truth) - fig.suptitle(uresult.summary(), fontsize="small") - return fig - - -# --------------------------------------------------------------------------- -# Substrate-thickness systematic -# --------------------------------------------------------------------------- -def _mark_thickness_prior(ax, uresult: UncertaintyResult) -> None: - """Shade the measured thickness prior ``L₀ ± σ_L`` (µm) on a thickness axis.""" - if uresult.thickness_central is None or uresult.thickness_sigma is None: - return - central_um = uresult.thickness_central / 1e-6 - sigma_um = uresult.thickness_sigma / 1e-6 - ax.axvspan( - central_um - sigma_um, - central_um + sigma_um, - color="0.8", - alpha=0.5, - label="L₀ ± σ_L", - ) - ax.axvline(central_um, color="0.4", ls=":", lw=1.0) - - -def plot_fwhm_vs_thickness( - ax, uresult: UncertaintyResult, *, truth: float | None = None -) -> None: - """Scatter the retrieved FWHM against the drawn substrate thickness. - - The horizontal bands give the point estimate and its 68 % interval; the vertical - band is the measured thickness prior. The slope of the cloud is the - sensitivity ``dFWHM/dL`` the analysis propagates; solver variation adds to - the spread. - - Parameters - ---------- - ax : matplotlib.axes.Axes - Axis to draw into. - uresult : UncertaintyResult - A ``method="thickness"`` result (its ``thicknesses`` are the x-values). - truth : float, optional - A known true FWHM (s) to mark with a dashed line (synthetic tests). - """ - if uresult.thicknesses is None: - raise ValueError("uresult has no thicknesses; run method='thickness'") - thick_um = np.asarray(uresult.thicknesses, dtype=float) / 1e-6 - fwhm_fs = np.asarray(uresult.samples, dtype=float) / 1e-15 - _mark_thickness_prior(ax, uresult) - ax.scatter(thick_um, fwhm_fs, s=14, color="C0", alpha=0.6, edgecolors="none") - ax.axhline(uresult.point_estimate / 1e-15, color="C3", lw=1.5, label="estimate") - lo68, hi68 = uresult.interval_68 - ax.axhspan(lo68 / 1e-15, hi68 / 1e-15, color="C3", alpha=0.15, label="68 %") - if truth is not None: - ax.axhline(truth / 1e-15, color="0.35", ls="--", lw=1.2, label="truth") - ax.set_xlabel("Substrate thickness (µm)") - ax.set_ylabel("FWHM (fs)") - ax.set_title("FWHM vs thickness") - ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") - - -def plot_error_vs_thickness(ax, uresult: UncertaintyResult) -> None: - """Scatter the FROG error against the drawn thickness (a consistency check). - - A flat cloud means the data do not constrain the thickness (the full prior - propagates); a clear minimum near ``L₀`` means the trace itself prefers a - thickness under the chosen model. The plot does not reweight the prior. - """ - if uresult.thicknesses is None: - raise ValueError("uresult has no thicknesses; run method='thickness'") - thick_um = np.asarray(uresult.thicknesses, dtype=float) / 1e-6 - errors = np.asarray(uresult.frog_errors, dtype=float) - _mark_thickness_prior(ax, uresult) - ax.scatter(thick_um, errors, s=14, color="C1", alpha=0.6, edgecolors="none") - ax.set_xlabel("Substrate thickness (µm)") - ax.set_ylabel("FROG error R") - ax.set_title("Trace misfit vs thickness") - ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") - - -def plot_thickness_sensitivity( - uresult: UncertaintyResult, - *, - truth: float | None = None, - figsize=(13.0, 4.2), - fig: Figure | None = None, -) -> Figure: - """Composite figure for the substrate-thickness systematic. - - Panels: FWHM vs thickness, FROG error vs thickness, and — when profiles were - collected — the temporal intensity confidence band. - - Parameters - ---------- - uresult : UncertaintyResult - A ``method="thickness"`` result. - truth : float, optional - Known true FWHM (s) for the FWHM panel (synthetic tests). - figsize : tuple, optional - Figure size. - fig : matplotlib.figure.Figure, optional - Existing figure to draw into (cleared first), e.g. a Qt canvas figure. - - Returns - ------- - matplotlib.figure.Figure - """ - fig = _prepare_fig(fig, figsize) - has_band = uresult.profiles is not None and uresult.t_profile is not None - axes = fig.subplots(1, 3 if has_band else 2) - plot_fwhm_vs_thickness(axes[0], uresult, truth=truth) - plot_error_vs_thickness(axes[1], uresult) - if has_band: - plot_intensity_band(axes[2], uresult) - fig.suptitle(uresult.summary(), fontsize="small") - return fig diff --git a/docs/conf.py b/docs/conf.py index 44b6d2b..7d93b9b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,8 +8,12 @@ from __future__ import annotations +import importlib +import pkgutil from pathlib import Path +from sphinx.pycode import ModuleAnalyzer + import croak # -- Project information ----------------------------------------------------- @@ -81,6 +85,31 @@ autodoc_typehints = "description" autodoc_member_order = "bysource" + +# croak.plotting is a package whose __init__.py re-exports what its submodules +# define. autodoc reads a constant's ``#:`` doc-comment only from the source of the +# module it documents -- here __init__.py, where a re-exported constant has none -- +# and silently drops the constant as undocumented. Seed autodoc's cached analysis +# of the package with the comments (and annotations) of the defining submodules. +def _adopt_submodule_attr_docs(package_name: str) -> None: + """Give ``package_name``'s analyzer the attribute docs of its submodules.""" + package = importlib.import_module(package_name) + analyzer = ModuleAnalyzer.for_module(package_name) + analyzer.analyze() + for info in pkgutil.iter_modules(package.__path__, f"{package_name}."): + submodule = ModuleAnalyzer.for_module(info.name) + submodule.analyze() + for (scope, name), comment in submodule.attr_docs.items(): + if scope == "" and name in package.__all__: + analyzer.attr_docs.setdefault((scope, name), comment) + if (scope, name) in submodule.annotations: + analyzer.annotations.setdefault( + (scope, name), submodule.annotations[scope, name] + ) + + +_adopt_submodule_attr_docs("croak.plotting") + napoleon_numpy_docstring = True napoleon_google_docstring = False napoleon_use_rtype = False From c3e25c27089a95c5242c0c10155cf19ac08dfc8a Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 21:37:42 +0000 Subject: [PATCH 04/41] Split croak.plotting into a package by domain croak/plotting.py (2660 lines; its deletion landed in the previous commit by mistake) becomes croak/plotting/ with modules scale, units, figure, artists, traces, pulse, panels, updates, retrieval and uncertainty. Bodies move verbatim; only imports change (one relative import inside _interaction_scale). The package re-exports the unchanged __all__ and every other public name, so croak.plotting.X keeps working. The tests reach private helpers through their new submodules and monkeypatch process_result and spectrogram where panels.py calls them. Co-Authored-By: Claude Opus 5.5 --- croak/plotting/__init__.py | 219 +++++++++++++ croak/plotting/artists.py | 156 ++++++++++ croak/plotting/figure.py | 17 + croak/plotting/panels.py | 436 ++++++++++++++++++++++++++ croak/plotting/pulse.py | 438 ++++++++++++++++++++++++++ croak/plotting/retrieval.py | 426 +++++++++++++++++++++++++ croak/plotting/scale.py | 152 +++++++++ croak/plotting/traces.py | 570 ++++++++++++++++++++++++++++++++++ croak/plotting/uncertainty.py | 233 ++++++++++++++ croak/plotting/units.py | 78 +++++ croak/plotting/updates.py | 240 ++++++++++++++ tests/test_plotting.py | 52 ++-- 12 files changed, 2995 insertions(+), 22 deletions(-) create mode 100644 croak/plotting/__init__.py create mode 100644 croak/plotting/artists.py create mode 100644 croak/plotting/figure.py create mode 100644 croak/plotting/panels.py create mode 100644 croak/plotting/pulse.py create mode 100644 croak/plotting/retrieval.py create mode 100644 croak/plotting/scale.py create mode 100644 croak/plotting/traces.py create mode 100644 croak/plotting/uncertainty.py create mode 100644 croak/plotting/units.py create mode 100644 croak/plotting/updates.py diff --git a/croak/plotting/__init__.py b/croak/plotting/__init__.py new file mode 100644 index 0000000..6a5fa27 --- /dev/null +++ b/croak/plotting/__init__.py @@ -0,0 +1,219 @@ +"""Matplotlib plotting for retrieval results. + +Provides custom colormaps (white at the low end; a red band for negatives), +reusable **single-axis** plotters (so the GUI embeds exactly the same code), and +**composite** figures: the 12-panel :func:`plot_retrieval`, the 6-panel +:func:`plot_frog_filter`, and :func:`plot_simulated_trace`. + +The retrieval summary is assembled from twelve independent **panels** +(``draw_(ax, data)`` over one :class:`RetrievalPlotData` bundle, registered in +:data:`RETRIEVAL_PANELS`). :func:`plot_retrieval` lays them out as the 3×4 +overview; :func:`plot_retrieval_page` draws one of the three 2×2 pages +(:data:`RETRIEVAL_PAGES`: Traces, Pulse, Diagnostics) the GUI shows as tabs. + +Figures are built with :external+matplotlib:class:`matplotlib.figure.Figure` directly +(no ``pyplot`` global state), so they embed cleanly in a Qt canvas and also save +standalone via ``fig.savefig(...)``. + +Every public name is importable from ``croak.plotting`` itself; the submodules +group them by domain: + +``scale`` + On-screen text density (:class:`PlotScale`, :func:`plot_scale`, + :func:`scaled_rc`). +``units`` + Display units and read-out formatting (:func:`sig3`, :func:`si_power`). +``traces`` + FROG-trace images: colormaps, :func:`signed_pcolormesh`, the residual and + marginal plotters, :func:`plot_frog_filter`, :class:`FrogFilterView` and + :func:`plot_simulated_trace`. +``pulse`` + The retrieved pulse: :func:`plot_temporal`, :func:`plot_spectral` and + :func:`plot_spectrogram`. +``artists`` + The artist bundles each panel returns (:class:`TraceArtists`, …). +``panels`` + :class:`RetrievalPlotData` and the twelve ``draw_`` panels. +``updates`` + The ``update_`` in-place twins of the panels, for the live preview. +``retrieval`` + The panel registry, pages and overview, :func:`plot_retrieval_page`, + :func:`plot_retrieval` and :class:`RetrievalPageView`. +``uncertainty`` + Bootstrap and substrate-thickness uncertainty figures. +``figure`` + The figure set-up the composites share. +""" + +from .artists import ( + ConvergenceArtists, + LineArtists, + MarginalArtists, + PanelArtists, + ResidualArtists, + SpectralArtists, + SpectrogramArtists, + TemporalArtists, + TraceArtists, +) +from .panels import ( + RetrievalPlotData, + draw_convergence, + draw_delay_marginal, + draw_freq_marginal, + draw_measured_lin, + draw_measured_log, + draw_residual, + draw_retrieved_lin, + draw_retrieved_log, + draw_spectral, + draw_spectral_filter, + draw_spectrogram, + draw_temporal, + plot_convergence, + retrieval_plot_data, +) +from .pulse import ( + EDGE_ENERGY_WARN, + SPECTRAL_AXES, + SpectralAxis, + plot_spectral, + plot_spectrogram, + plot_temporal, +) +from .retrieval import ( + RETRIEVAL_COLORBARS, + RETRIEVAL_OVERVIEW, + RETRIEVAL_PAGES, + RETRIEVAL_PANELS, + ColorbarSpec, + PageSpec, + PanelDraw, + PanelSpec, + PanelUpdate, + RetrievalPageView, + plot_retrieval, + plot_retrieval_page, +) +from .scale import ( + COMPACT_BELOW, + FONT_SCALE_MIN, + FONT_SCALE_STEP, + FULL_SCALE, + PlotScale, + plot_scale, + scaled_rc, +) +from .traces import ( + FrogFilterView, + cmap_negwhite, + cmap_white, + filter_view_vmax, + plot_frog_filter, + plot_marginal, + plot_residual, + plot_simulated_trace, + signed_pcolormesh, +) +from .uncertainty import ( + plot_error_vs_thickness, + plot_fwhm_distribution, + plot_fwhm_vs_thickness, + plot_intensity_band, + plot_thickness_sensitivity, + plot_uncertainty, +) +from .units import ( + si_power, + sig3, +) +from .updates import ( + update_convergence, + update_convergence_curve, + update_delay_marginal, + update_freq_marginal, + update_measured_lin, + update_measured_log, + update_residual, + update_retrieved_lin, + update_retrieved_log, + update_spectral, + update_spectral_filter, + update_spectrogram, + update_temporal, +) + +__all__ = [ + "PlotScale", + "FULL_SCALE", + "FONT_SCALE_MIN", + "FONT_SCALE_STEP", + "COMPACT_BELOW", + "plot_scale", + "scaled_rc", + "cmap_white", + "cmap_negwhite", + "signed_pcolormesh", + "si_power", + "sig3", + "plot_residual", + "plot_temporal", + "plot_spectral", + "SPECTRAL_AXES", + "EDGE_ENERGY_WARN", + "plot_convergence", + "plot_marginal", + "plot_spectrogram", + "plot_retrieval", + "RetrievalPlotData", + "retrieval_plot_data", + "TraceArtists", + "ResidualArtists", + "TemporalArtists", + "SpectralArtists", + "ConvergenceArtists", + "LineArtists", + "MarginalArtists", + "SpectrogramArtists", + "PanelSpec", + "PageSpec", + "ColorbarSpec", + "RETRIEVAL_PANELS", + "RETRIEVAL_COLORBARS", + "RETRIEVAL_PAGES", + "RETRIEVAL_OVERVIEW", + "plot_retrieval_page", + "draw_measured_log", + "draw_retrieved_log", + "draw_measured_lin", + "draw_retrieved_lin", + "draw_temporal", + "draw_spectral", + "draw_convergence", + "draw_residual", + "draw_spectral_filter", + "draw_freq_marginal", + "draw_delay_marginal", + "draw_spectrogram", + "update_measured_log", + "update_retrieved_log", + "update_measured_lin", + "update_retrieved_lin", + "update_temporal", + "update_spectral", + "update_convergence", + "update_convergence_curve", + "update_residual", + "update_spectral_filter", + "update_freq_marginal", + "update_delay_marginal", + "update_spectrogram", + "RetrievalPageView", + "plot_frog_filter", + "filter_view_vmax", + "FrogFilterView", + "plot_simulated_trace", + "plot_fwhm_distribution", + "plot_intensity_band", + "plot_uncertainty", +] diff --git a/croak/plotting/artists.py b/croak/plotting/artists.py new file mode 100644 index 0000000..50fca25 --- /dev/null +++ b/croak/plotting/artists.py @@ -0,0 +1,156 @@ +"""The matplotlib artists each retrieval panel returns, for in-place updates.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from matplotlib.axes import Axes + from matplotlib.collections import QuadMesh + from matplotlib.legend import Legend + from matplotlib.lines import Line2D + from matplotlib.text import Text + + +# --------------------------------------------------------------------------- +# Panel artists +# --------------------------------------------------------------------------- +# Each retrieval panel returns the matplotlib artists it created, in a small frozen +# dataclass, so that a live view can later update them in place (set_data, +# set_array, set_text) instead of clearing and rebuilding the figure, and so that +# page-level decorations (the colorbars shared by a pair of trace panels) can find +# their mappables. eq=False: comparing artists by field value is never meaningful. + + +@dataclass(frozen=True, eq=False) +class TraceArtists: + """Artists of a trace-image panel. + + Attributes + ---------- + mesh_pos, mesh_neg : QuadMesh + The positive-sample image and, when the data hold negatives, the + white→red magnitude image of the negative samples (else ``None``). + label : Text or None + The bold error/shape annotation of the retrieved-linear panel. + """ + + mesh_pos: QuadMesh + mesh_neg: QuadMesh | None + label: Text | None = None + + +@dataclass(frozen=True, eq=False) +class ResidualArtists: + """Artists of the residual panel: its signed image (``None`` if no residual).""" + + mesh: QuadMesh | None + + +@dataclass(frozen=True, eq=False) +class TemporalArtists: + """Artists of the temporal panel. + + Attributes + ---------- + line_retr, line_tl : Line2D + Retrieved and transform-limited intensity (or power) curves. + line_phase : Line2D + Retrieved temporal phase on the twin axis ``ax_phase``. + ax_phase : Axes + The right-hand phase axis. + legend : Legend + The combined intensity + phase legend. + legend_retr, legend_tl : Text + The legend entries carrying the R and TL durations (they change with the + result, so a live update rewrites them in place). + line_truth, line_truth_phase : Line2D or None + Ground-truth intensity and phase overlays, when a truth was given. + """ + + line_retr: Line2D + line_tl: Line2D + line_phase: Line2D + ax_phase: Axes + legend: Legend + legend_retr: Text + legend_tl: Text + line_truth: Line2D | None = None + line_truth_phase: Line2D | None = None + + +@dataclass(frozen=True, eq=False) +class SpectralArtists: + """Artists of the spectrum panel. + + Attributes + ---------- + line_retr : Line2D + Retrieved spectral intensity. + line_phase, line_phase_fit : Line2D + Retrieved spectral phase and its dashed polynomial fit, on ``ax_phase``. + ax_phase : Axes + The right-hand phase axis. + legend : Legend + The combined intensity + phase legend. + text_dispersion, text_edge : Text + The GDD/TOD block and the edge-energy warning. + line_meas, line_truth, line_truth_phase : Line2D or None + Measured spectrum, truth spectrum and truth phase, when available. + """ + + line_retr: Line2D + line_phase: Line2D + line_phase_fit: Line2D + ax_phase: Axes + legend: Legend + text_dispersion: Text + text_edge: Text + line_meas: Line2D | None = None + line_truth: Line2D | None = None + line_truth_phase: Line2D | None = None + + +@dataclass(frozen=True, eq=False) +class ConvergenceArtists: + """Artists of the convergence panel: the error curve, stage rules, legend.""" + + line: Line2D + boundaries: tuple[Line2D, ...] = () + legend: Legend | None = None + + +@dataclass(frozen=True, eq=False) +class LineArtists: + """A single-curve panel (the spectral filter).""" + + line: Line2D + + +@dataclass(frozen=True, eq=False) +class MarginalArtists: + """Artists of a marginal panel: retrieved and (optional) measured curves.""" + + line_retr: Line2D + legend: Legend + line_meas: Line2D | None = None + + +@dataclass(frozen=True, eq=False) +class SpectrogramArtists: + """Artists of the spectrogram panel: its image.""" + + mesh: QuadMesh + + +type PanelArtists = ( + TraceArtists + | ResidualArtists + | TemporalArtists + | SpectralArtists + | ConvergenceArtists + | LineArtists + | MarginalArtists + | SpectrogramArtists +) diff --git a/croak/plotting/figure.py b/croak/plotting/figure.py new file mode 100644 index 0000000..f5da62f --- /dev/null +++ b/croak/plotting/figure.py @@ -0,0 +1,17 @@ +"""Figure lifecycle shared by the composite plotters.""" + +from __future__ import annotations + +import contextlib + +from matplotlib.figure import Figure + + +def _prepare_fig(fig, figsize): + """Return a cleared figure to draw a composite into (new if None).""" + if fig is None: + return Figure(figsize=figsize, layout="constrained") + fig.clear() + with contextlib.suppress(Exception): + fig.set_layout_engine("constrained") + return fig diff --git a/croak/plotting/panels.py b/croak/plotting/panels.py new file mode 100644 index 0000000..4c990b1 --- /dev/null +++ b/croak/plotting/panels.py @@ -0,0 +1,436 @@ +"""The twelve retrieval panels over one shared :class:`RetrievalPlotData` bundle.""" + +from __future__ import annotations + +import functools +from collections.abc import Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import numpy as np +from numpy.typing import ArrayLike + +from ..maths import wlfreq +from ..processing import ProcessedResult, TruthPulse, process_result, spectrogram +from ..result import RetrievalResult +from .artists import ( + ConvergenceArtists, + LineArtists, + MarginalArtists, + ResidualArtists, + SpectralArtists, + SpectrogramArtists, + TemporalArtists, + TraceArtists, +) +from .pulse import SPECTRAL_AXES, _spectral_panel, _temporal_panel, plot_spectrogram +from .traces import cmap_white, plot_marginal, plot_residual, signed_pcolormesh +from .units import _TWOPI_PHZ, si_power + +if TYPE_CHECKING: + from matplotlib.collections import QuadMesh + from matplotlib.colors import Colormap + from matplotlib.lines import Line2D + from numpy.typing import NDArray + + +# --------------------------------------------------------------------------- +# Retrieval panels: the data bundle and the twelve panels +# --------------------------------------------------------------------------- +# The twelve-panel retrieval summary is built from independent panels so the GUI +# can arrange them as three 2×2 pages (Traces / Pulse / Diagnostics) while scripts +# and the CLI keep the 3×4 overview. Every panel is a pure function +# ``draw_(ax, data)`` over one immutable RetrievalPlotData, computed once per +# result. Colorbars that span two panels are a page-level concern (ColorbarSpec, +# in croak.plotting.retrieval), drawn after all panels of a figure exist. + + +@dataclass(frozen=True, eq=False) +class RetrievalPlotData: + """Everything the retrieval panels share, computed once from one result. + + Build it with :func:`retrieval_plot_data`. The bundle is immutable and carries + no figure state, so one instance can feed several figures (the GUI's pages and + their pop-outs) and be redrawn at any size. + + Attributes + ---------- + result : RetrievalResult + The retrieval; ``result.trace`` is the retrieved trace. + processed : ProcessedResult + Display-ready quantities of ``result`` + (:func:`croak.processing.process_result`). + measured, retrieved : ndarray + Measured and retrieved traces, shape ``(Nω, Nτ)``. + omega0_trace : float + Centre angular frequency of the *signal* (twice ``omega0`` for SHG), so + the trace panels' frequency axis reads at the signal frequency. + halfwidth : float + Half the delay span (s); bounds the delay axes. + lam_min, lam_max : float + Wavelength bounds (m) of the spectrum and spectrogram panels. + flim : tuple of float or None + Optional ``(f_min, f_max)`` (Hz) bounds of the trace/residual frequency axes. + tracedb : float + Dynamic range (dB) of the logarithmic trace panels. + cmap_pos : Colormap + White-based positive colormap shared by the trace and spectrogram panels. + lin_vmax : float + Common scale of the linear trace pair, so their colorbar is meaningful. + truth : TruthPulse or None + Known pulse to overlay (synthetic and simulated data). + power_scale : tuple of (float, str) or None + SI ``(scale, unit)`` for absolute power when a pulse energy was given. + spectral_axis : str + ``"frequency"`` or ``"wavelength"``: the spectrum panel's abscissa. + """ + + result: RetrievalResult + processed: ProcessedResult + measured: NDArray[np.float64] + retrieved: NDArray[np.float64] + omega0_trace: float + halfwidth: float + lam_min: float + lam_max: float + flim: tuple[float, float] | None + tracedb: float + cmap_pos: Colormap + lin_vmax: float + truth: TruthPulse | None + power_scale: tuple[float, str] | None + spectral_axis: str + + @functools.cached_property + def spectrogram( + self, + ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: + """``(t_centres, wavelength, S)`` of :func:`croak.processing.spectrogram`. + + The Gabor transform is the costliest thing the panels compute (hundreds + of FFTs), so it runs only when the spectrogram panel is first drawn and is + then cached on the bundle for every later draw of it. + """ + return spectrogram(self.result, lam_min=self.lam_min, lam_max=self.lam_max) + + +def retrieval_plot_data( + result: RetrievalResult, + *, + measured: ArrayLike | None = None, + Iomega_meas: ArrayLike | None = None, + lam_min: float | None = None, + lam_max: float | None = None, + flim: tuple[float, float] | None = None, + tracedb: float = 30.0, + cmap: str = "viridis", + truth: TruthPulse | None = None, + energy: float | None = None, + processed: ProcessedResult | None = None, + spectral_axis: str = "frequency", +) -> RetrievalPlotData: + """Bundle a retrieval's plot inputs for the panels (see :func:`plot_retrieval`). + + The arguments are those of :func:`plot_retrieval`, which is this bundle drawn + as the overview; see there for their meaning. ``processed`` is an + already-computed :class:`~croak.processing.ProcessedResult` **for this same + result** (the GUI computes it once for its read-out); it must have been built + with the same ``measured``/``Iomega_meas``/``energy``. + + Raises + ------ + ValueError + If ``result`` carries no trace, ``processed`` belongs to another result, + or ``spectral_axis`` is not one of :data:`SPECTRAL_AXES`. + """ + omega = result.omega + omega0 = result.omega0 + if spectral_axis not in SPECTRAL_AXES: + raise ValueError( + f"spectral_axis must be one of {SPECTRAL_AXES}, got {spectral_axis!r}" + ) + if result.trace is None: + raise ValueError("plot_retrieval requires a result carrying a simulated trace") + retrieved = np.asarray(result.trace, dtype=float) + measured_arr = retrieved if measured is None else np.asarray(measured, dtype=float) + + if processed is None: + pr = process_result( + result, measured=measured_arr, Iomega_meas=Iomega_meas, energy=energy + ) + elif processed.result is not result: + # Cheap exact guard: mixing a ProcessedResult from another result would + # draw the trace panels from one retrieval and the pulse/spectrum panels + # from another, which is very hard to spot by eye. + raise ValueError("processed must be the ProcessedResult of this result") + else: + pr = processed + if lam_min is None or lam_max is None: + lams = wlfreq(omega + omega0) + lam_min, lam_max = float(lams.min()), float(lams.max()) + delays = result.delays + # When a pulse energy was supplied, derive one SI power prefix for the + # temporal panel (and the truth overlay) so both share a consistent axis. + power_scale = None + if pr.peak_power is not None: + power_scale = si_power(max(pr.peak_power, pr.peak_power_tl or 0.0)) + return RetrievalPlotData( + result=result, + processed=pr, + measured=measured_arr, + retrieved=retrieved, + omega0_trace=omega0 * _interaction_scale(result.interaction), + halfwidth=float((delays.max() - delays.min()) / 2), + lam_min=float(lam_min), + lam_max=float(lam_max), + flim=flim, + tracedb=tracedb, + # Positive values on the base map with white at the low end, so zero is + # white in *both* maps and meets the white→red negative map seamlessly. + cmap_pos=cmap_white(cmap), + # The linear pair shares one scale so its colorbar is meaningful. + lin_vmax=max( + float(np.nanmax(np.abs(measured_arr))), float(np.nanmax(retrieved)) + ), + truth=truth, + power_scale=power_scale, + spectral_axis=spectral_axis, + ) + + +def plot_convergence( + ax, errors: ArrayLike, *, boundaries: Sequence[int] = () +) -> ConvergenceArtists: + """Plot the FROG error vs iteration on a log y-axis. + + ``boundaries`` are indices into ``errors`` where a multi-stage solver handed + over to its next stage (:attr:`~croak.result.RetrievalResult.stage_boundaries`); + each is drawn as a dashed rule. Marking them matters because the stages need + not count the same unit of work — ``warm-lbfgs`` splices COPRA iterations onto + L-BFGS function evaluations — so an unmarked kink would read as convergence + behaviour rather than as the change of algorithm it is. + + The error curve is created even for an empty ``errors`` (a result restored + from file carries no history), so a live view can grow it in place. + """ + errors = np.asarray(errors, dtype=float) + (line,) = ax.semilogy(np.arange(1, errors.size + 1), errors, c="C0") + rules: list[Line2D] = [] + for i, edge in enumerate(boundaries): + if 0 < edge < errors.size: + rules.append( + ax.axvline( + edge + 0.5, + c="0.5", + ls="--", + lw=0.8, + label="stage handover" if i == 0 else None, + ) + ) + legend = None + if rules: # upper right: a decreasing curve leaves that corner empty + legend = ax.legend( + loc="upper right", handlelength=1.2, frameon=False, fontsize="small" + ) + ax.set_xlabel("Iteration") + ax.set_ylabel("Error") + ax.set_title("Convergence") + return ConvergenceArtists(line=line, boundaries=tuple(rules), legend=legend) + + +def _trace_label(data: RetrievalPlotData) -> str: + """Return the retrieved-linear panel's annotation: FROG error and shape.""" + return ( + f"{data.result.error * 100:.2f}%\n" + f"{data.retrieved.shape[0]}×{data.retrieved.shape[1]}" + ) + + +def _draw_trace_image( + ax, data: RetrievalPlotData, C: NDArray[np.float64], *, db: bool, title: str +) -> tuple[QuadMesh, QuadMesh | None]: + """Draw one trace image (delay × signal frequency) and return its meshes.""" + mpos, mneg = signed_pcolormesh( + ax, + data.result.delays / 1e-15, + (data.result.omega + data.omega0_trace) / _TWOPI_PHZ, + C, + db=db, + tracedb=data.tracedb, + cmap_pos=data.cmap_pos, + vmax=None if db else data.lin_vmax, + ) + ax.set_xlabel("Delay (fs)") + ax.set_ylabel("Frequency (PHz)") + ax.set_title(title) + ax.set_xlim(-data.halfwidth / 1e-15, data.halfwidth / 1e-15) + if data.flim is not None: + ax.set_ylim(data.flim[0] / 1e15, data.flim[1] / 1e15) + return mpos, mneg + + +def draw_measured_log(ax, data: RetrievalPlotData) -> TraceArtists: + """Draw the measured trace, logarithmic (``tracedb`` dynamic range).""" + mpos, mneg = _draw_trace_image( + ax, data, data.measured, db=True, title="Measured (log)" + ) + return TraceArtists(mesh_pos=mpos, mesh_neg=mneg) + + +def draw_retrieved_log(ax, data: RetrievalPlotData) -> TraceArtists: + """Draw the retrieved trace, logarithmic.""" + mpos, mneg = _draw_trace_image( + ax, data, data.retrieved, db=True, title="Retrieved (log)" + ) + return TraceArtists(mesh_pos=mpos, mesh_neg=mneg) + + +def draw_measured_lin(ax, data: RetrievalPlotData) -> TraceArtists: + """Draw the measured trace, linear, on the scale shared with the retrieved.""" + mpos, mneg = _draw_trace_image( + ax, data, data.measured, db=False, title="Measured (lin)" + ) + return TraceArtists(mesh_pos=mpos, mesh_neg=mneg) + + +def draw_retrieved_lin(ax, data: RetrievalPlotData) -> TraceArtists: + """Draw the retrieved trace, linear, annotated with FROG error and shape.""" + mpos, mneg = _draw_trace_image( + ax, data, data.retrieved, db=False, title="Retrieved (lin)" + ) + label = ax.text( + 0.04, + 0.96, + _trace_label(data), + transform=ax.transAxes, + ha="left", + va="top", + color="black", + fontweight="bold", + fontsize="small", + ) + return TraceArtists(mesh_pos=mpos, mesh_neg=mneg, label=label) + + +def draw_temporal(ax, data: RetrievalPlotData) -> TemporalArtists: + """Draw the retrieved pulse: intensity/power, TL pulse, phase, truth overlay.""" + return _temporal_panel( + ax, + data.processed, + halfwidth=data.halfwidth, + power_scale=data.power_scale, + truth=data.truth, + ) + + +def draw_spectral(ax, data: RetrievalPlotData) -> SpectralArtists: + """Draw the spectrum: retrieved, measured, phase and fit, GDD/TOD, truth.""" + return _spectral_panel( + ax, + data.processed, + lam_min=data.lam_min, + lam_max=data.lam_max, + truth=data.truth, + axis=data.spectral_axis, + ) + + +def draw_convergence(ax, data: RetrievalPlotData) -> ConvergenceArtists: + """Draw the FROG error against iteration, with the solver's stage handovers.""" + return plot_convergence( + ax, data.processed.errors, boundaries=data.result.stage_boundaries + ) + + +def draw_residual(ax, data: RetrievalPlotData) -> ResidualArtists: + """Draw the measured − retrieved residual; leave it empty if there is none.""" + pr = data.processed + if pr.residual is None: + return ResidualArtists(mesh=None) + mesh = plot_residual( + ax, + data.result.omega, + data.result.delays, + pr.residual, + data.result.omega0, + halfwidth=data.halfwidth, + flim=data.flim, + ) + return ResidualArtists(mesh=mesh) + + +def _filter_factor(result: RetrievalResult) -> NDArray[np.float64]: + """Return the per-frequency Rω scale factor, peak-normalised, else flat.""" + mu = np.atleast_1d(np.asarray(result.mu, dtype=float)) + if mu.size == result.omega.size: + return mu / mu.max() + return np.ones_like(result.omega) + + +def draw_spectral_filter(ax, data: RetrievalPlotData) -> LineArtists: + """Draw the per-frequency scale factor (Rω filter) against wavelength.""" + # A centred retrieval grid can cross zero frequency, where the wavelength + # axis has a pole: harmless for the plot, so the divide warning is silenced. + with np.errstate(divide="ignore", invalid="ignore"): + lam_nm = wlfreq(data.result.omega + data.result.omega0) / 1e-9 + (line,) = ax.plot(lam_nm, _filter_factor(data.result), c="C2") + ax.set_xlabel("Wavelength (nm)") + ax.set_ylabel("Filter factor") + ax.set_title("Spectral filter") + ax.set_xlim(data.lam_min / 1e-9, data.lam_max / 1e-9) + return LineArtists(line=line) + + +def draw_freq_marginal(ax, data: RetrievalPlotData) -> MarginalArtists: + """Draw the frequency marginal of the retrieved and measured traces.""" + pr = data.processed + artists = plot_marginal( + ax, + data.result.omega + data.result.omega0, + pr.omega_marg_retr, + pr.omega_marg_meas, + xlabel="Frequency (PHz)", + scale=_TWOPI_PHZ, + ) + ax.set_title("Frequency marginal") + return artists + + +def draw_delay_marginal(ax, data: RetrievalPlotData) -> MarginalArtists: + """Draw the delay marginal of the retrieved and measured traces.""" + pr = data.processed + artists = plot_marginal( + ax, + data.result.delays, + pr.tau_marg_retr, + pr.tau_marg_meas, + xlabel="Delay (fs)", + scale=1e-15, + halfwidth=data.halfwidth, + ) + ax.set_title("Delay marginal") + return artists + + +def draw_spectrogram(ax, data: RetrievalPlotData) -> SpectrogramArtists: + """Draw the Gabor spectrogram of the retrieved pulse (computed once, cached).""" + tc, lam, S = data.spectrogram + mesh = plot_spectrogram( + ax, + tc, + lam, + S, + lam_min=data.lam_min, + lam_max=data.lam_max, + halfwidth=data.halfwidth, + # white at zero, like the trace panels and the dispersion stage's own + # spectrogram — this panel was the only one left on plain viridis. + cmap=data.cmap_pos, + ) + return SpectrogramArtists(mesh=mesh) + + +def _interaction_scale(name: str) -> float: + from ..interactions import get_interaction + + return get_interaction(name).omega0_scale diff --git a/croak/plotting/pulse.py b/croak/plotting/pulse.py new file mode 100644 index 0000000..0006c12 --- /dev/null +++ b/croak/plotting/pulse.py @@ -0,0 +1,438 @@ +"""The retrieved pulse: temporal, spectral and spectrogram panels.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import numpy as np + +from ..maths import wlfreq +from ..processing import ProcessedResult, TruthPulse +from .artists import SpectralArtists, TemporalArtists +from .traces import cmap_white +from .units import _TWOPI_PHZ, _fs, si_power + +if TYPE_CHECKING: + from matplotlib.legend import Legend + from numpy.typing import NDArray + + +#: Edge-energy fraction (see :func:`croak.processing.edge_energy_fraction`) above +#: which the spectrum panel flags the retrieval in red. A sound retrieval on a +#: generous band sits two orders of magnitude below this; above it, re-run on a +#: wider grid. +EDGE_ENERGY_WARN = 0.01 + + +#: Abscissae the spectrum panel can be drawn against (see :func:`plot_spectral`). +SPECTRAL_AXES: tuple[str, ...] = ("frequency", "wavelength") +type SpectralAxis = Literal["frequency", "wavelength"] + + +def _combined_legend(ax, axp, *, loc: str) -> Legend: + """Draw one legend containing intensity and twin-axis phase curves. + + ``loc`` is a fixed corner: matplotlib's ``"best"`` re-evaluates on every draw + and made the legend jump between frames of the live preview. + """ + handles, labels = ax.get_legend_handles_labels() + phase_handles, phase_labels = axp.get_legend_handles_labels() + return ax.legend( + handles + phase_handles, + labels + phase_labels, + loc=loc, + handlelength=1.0, + frameon=False, + fontsize="small", + ) + + +def _temporal_curves(pr: ProcessedResult, power_scale): + """Return ``(retr, tl, r_label, tl_label, ylabel, power_scale)`` for the pulse. + + In absolute power when ``pr.peak_power`` is known (deriving the SI prefix if + ``power_scale`` is None), else normalised. Shared by draw and update. + """ + if pr.peak_power is not None: + if power_scale is None: + power_scale = si_power(max(pr.peak_power, pr.peak_power_tl or 0.0)) + scale, unit = power_scale + retr = pr.It_retr * pr.peak_power / scale + tl = pr.It_tl * (pr.peak_power_tl or 0.0) / scale + r_label = f"R ({_fs(pr.fwhm_retr)} fs, {pr.peak_power / scale:.1f} {unit})" + tl_label = ( + f"TL ({_fs(pr.fwhm_tl)} fs, {(pr.peak_power_tl or 0.0) / scale:.1f} {unit})" + ) + return retr, tl, r_label, tl_label, f"Power ({unit})", power_scale + r_label = f"R ({_fs(pr.fwhm_retr)} fs)" + tl_label = f"TL ({_fs(pr.fwhm_tl)} fs)" + return pr.It_retr, pr.It_tl, r_label, tl_label, "Power (a.u.)", None + + +def _truth_intensity(truth: TruthPulse, energy, power_scale) -> NDArray[np.float64]: + """Return the truth's intensity on the temporal panel's scale. + + With a pulse energy the truth is shown in absolute power on the shared SI + scale: it carries the same energy, so its peak power is energy/∫(I/I_peak)dt + over its *own* duration (:meth:`~croak.processing.TruthPulse.peak_power`). + """ + peak_power = truth.peak_power(energy) + if peak_power is None: + return np.asarray(truth.It, dtype=float) + scale = power_scale[0] if power_scale is not None else 1.0 + return np.asarray(truth.It, dtype=float) * peak_power / scale + + +def _intensity_ylim(*curves) -> tuple[float, float]: + """``(0, 1.05 × max)`` over the given curves (``None`` entries ignored). + + Set explicitly because ``Axes.set_ylim`` switches autoscaling off, so an + in-place update could not rely on it; the 5 % headroom is matplotlib's + default margin. + """ + tops = [float(np.nanmax(c)) for c in curves if c is not None and np.size(c)] + top = max(tops, default=1.0) + return 0.0, 1.05 * top if top > 0 else 1.0 + + +def _phase_ylim(*curves) -> tuple[float, float] | None: + """Phase-axis limits centred on the shown phases with their span as margin. + + Finite samples of every non-empty curve count; ``None`` when nothing is shown. + """ + arrays = [np.asarray(c, dtype=float) for c in curves if c is not None] + arrays = [a[np.isfinite(a)] for a in arrays] + arrays = [a for a in arrays if a.size] + if not arrays: + return None + pm = np.concatenate(arrays) + d = pm.max() - pm.min() + mid = (pm.max() + pm.min()) / 2 + return mid - d - 1e-9, mid + d + 1e-9 + + +def _temporal_panel( + ax, + pr: ProcessedResult, + *, + halfwidth=None, + power_scale=None, + truth: TruthPulse | None = None, +) -> TemporalArtists: + """Draw the temporal panel into ``ax``; return its artists (see plot_temporal).""" + t = pr.t_over / 1e-15 + retr, tl, r_label, tl_label, ylabel, power_scale = _temporal_curves(pr, power_scale) + (line_retr,) = ax.plot(t, retr, c="C0", label=r_label) + # The TL pulse is peak-centred on its own oversampled axis (its peak index + # need not coincide with the retrieved pulse's), so plot it against t_tl — + # using t_over would shift its peak off t=0 by a sample or two. + (line_tl,) = ax.plot(pr.t_tl / 1e-15, tl, c="C1", label=tl_label) + line_truth = None + truth_curve = None + if truth is not None: + # The known pulse as a thin black line *under* the retrieved/TL curves, in + # physical units so it is independent of the retrieval grid. A retrieval + # is defined only up to the time-direction ambiguity, so the truth may + # appear shifted or flipped. Its legend entry carries its own FWHM so the + # three durations read side by side. + truth_curve = _truth_intensity(truth, pr.energy, power_scale) + (line_truth,) = ax.plot( + truth.t / 1e-15, + truth_curve, + c="k", + lw=0.8, + zorder=1, + label=f"truth ({_fs(truth.fwhm)} fs)", + ) + ax.set_xlabel("Time (fs)") + ax.set_ylabel(ylabel) + ax.set_ylim(*_intensity_ylim(retr, tl, truth_curve)) + ax.set_title("Retrieved pulse") + if halfwidth is not None: + ax.set_xlim(-halfwidth / 1e-15, halfwidth / 1e-15) + axp = ax.twinx() + (line_phase,) = axp.plot(t[pr.mask_t], pr.phi_t[pr.mask_t], c="C2") + axp.set_ylabel("Phase (rad)") + line_truth_phase = None + truth_phase = None + if truth is not None and truth.phi_t is not None: + truth_mask = np.asarray(truth.It) > 0.01 + truth_phase = np.asarray(truth.phi_t)[truth_mask] + (line_truth_phase,) = axp.plot( + np.asarray(truth.t)[truth_mask] / 1e-15, + truth_phase, + ":", + c="0.35", + lw=1.2, + label="truth phase", + ) + limits = _phase_ylim(pr.phi_t[pr.mask_t], truth_phase) + if limits is not None: + axp.set_ylim(*limits) + # Upper left: the pulse and its masked phase sit at the centre, the wings are + # empty, and a fixed corner cannot jump between live frames. + legend = _combined_legend(ax, axp, loc="upper left") + texts = legend.get_texts() + return TemporalArtists( + line_retr=line_retr, + line_tl=line_tl, + line_phase=line_phase, + ax_phase=axp, + legend=legend, + legend_retr=texts[0], + legend_tl=texts[1], + line_truth=line_truth, + line_truth_phase=line_truth_phase, + ) + + +def plot_temporal( + ax, + pr: ProcessedResult, + *, + halfwidth=None, + power_scale=None, + truth: TruthPulse | None = None, +): + """Temporal intensity/power (left) and phase (right twin axis); phase masked. + + When ``pr.peak_power`` is set (a measured pulse energy was supplied to + :func:`~croak.processing.process_result`), the intensity is shown as absolute + instantaneous power on an SI-prefixed axis; otherwise it stays normalised + ("a.u."). ``power_scale`` is an optional ``(scale, unit)`` pair (see + :func:`si_power`) so several panels can share one prefix; when ``None`` it is + derived from the larger of the retrieved/TL peak powers. + + ``truth`` (a :class:`~croak.processing.TruthPulse`: a synthetic generating + field, or a simulated scan's reference intensity) is overlaid as a thin black + line under the retrieved/TL curves, labelled with its own FWHM, and — when it + carries a temporal phase — as a dotted phase in the same display gauge as the + retrieved one (see :func:`croak.processing.normalize_temporal_phase`). + + Returns the twin phase axes. :func:`draw_temporal` is the same panel for the + registry, returning every artist (:class:`TemporalArtists`). + """ + return _temporal_panel( + ax, pr, halfwidth=halfwidth, power_scale=power_scale, truth=truth + ).ax_phase + + +def _truth_spectral_phase( + wavelength: NDArray[np.float64], truth: TruthPulse | None +) -> NDArray[np.float64] | None: + """Return the truth's spectral phase interpolated onto ``wavelength``, or None. + + The truth is stored on the simulation's own frequency grid, which is neither + the same spacing nor the same extent as the retrieval's; NaN outside it. + """ + tphi_src = getattr(truth, "phi_w", None) if truth is not None else None + tlam_src = getattr(truth, "lam", None) if truth is not None else None + if tphi_src is None or tlam_src is None: + return None + return np.interp( + wavelength, + np.asarray(tlam_src, dtype=float), + np.asarray(tphi_src, dtype=float), + left=np.nan, + right=np.nan, + ) + + +def _dispersion_text(pr: ProcessedResult) -> str: + """Return the GDD/TOD annotation of the spectrum panel.""" + return f"GDD: {pr.gdd_fs2:.1f} fs²\nTOD: {pr.tod_fs3:.1f} fs³" + + +def _edge_text(pr: ProcessedResult) -> tuple[str, str]: + """Return the edge-energy annotation and its colour (red once it matters). + + Energy parked in the unmeasured grid-edge bins is an artefact indicator; the + 1 % threshold sits comfortably above what a sound retrieval produces. + """ + color = "C3" if pr.edge_energy > EDGE_ENERGY_WARN else "0.35" + return f"Edge: {100 * pr.edge_energy:.2f} %", color + + +def _unit_peak(values: NDArray[np.float64]) -> NDArray[np.float64]: + """Scale ``values`` to a peak of one (NaNs ignored; all-zero left alone).""" + peak = float(np.nanmax(values)) if values.size else 0.0 + return values / peak if peak > 0 else values + + +def _spectral_curves(pr: ProcessedResult, axis: str): + """Return ``(x, retrieved, measured, xlabel)`` of the spectrum panel on ``axis``. + + Both spectra are unit-peak densities *in the chosen variable*. On the + wavelength axis they are ``pr.Ilam`` and ``pr.Iw_meas`` — both wavelength + densities (the measured one carries the ω² Jacobian despite its name); on the + frequency axis the retrieved spectrum is ``|E(ω)|²`` itself and the measured + one has that Jacobian divided out again, so a spectrum symmetric in ω plots + symmetric. ``x`` is per grid index on both axes, so the phase curves index it + with ``pr.mask_w`` unchanged. + """ + if axis == "wavelength": + return pr.wavelength / 1e-9, pr.Ilam, pr.Iw_meas, "Wavelength (nm)" + if axis != "frequency": + raise ValueError(f"axis must be one of {SPECTRAL_AXES}, got {axis!r}") + omega_abs = pr.omega + pr.omega0 + measured = None + if pr.Iw_meas is not None: + # A centred grid can cross ω = 0; that sample is undefined as a density. + with np.errstate(divide="ignore", invalid="ignore"): + jacobian = np.where(omega_abs != 0, 1.0 / omega_abs**2, np.nan) + measured = _unit_peak(pr.Iw_meas * jacobian) + return omega_abs / _TWOPI_PHZ, _unit_peak(pr.Iw), measured, "Frequency (PHz)" + + +def _spectral_xlim(lam_min, lam_max, axis: str) -> tuple[float, float] | None: + """Return the spectrum panel's x-limits for a wavelength window on ``axis``.""" + if not (lam_min and lam_max): + return None + if axis == "wavelength": + return lam_min / 1e-9, lam_max / 1e-9 + return float(wlfreq(lam_max)) / _TWOPI_PHZ, float(wlfreq(lam_min)) / _TWOPI_PHZ + + +def _truth_spectrum(truth: TruthPulse, axis: str): + """Return the truth's spectrum ``(x, unit-peak density)`` on ``axis``, or None.""" + if truth.lam is None or truth.Iw is None: + return None + if axis == "wavelength": + return truth.lam / 1e-9, truth.Iw + omega = wlfreq(truth.lam) + return omega / _TWOPI_PHZ, _unit_peak(np.asarray(truth.Iw) / omega**2) + + +def _spectral_panel( + ax, + pr: ProcessedResult, + *, + lam_min=None, + lam_max=None, + truth: TruthPulse | None = None, + axis: str = "frequency", +) -> SpectralArtists: + """Draw the spectrum panel into ``ax``; return its artists (see plot_spectral).""" + x, retrieved, measured, xlabel = _spectral_curves(pr, axis) + (line_retr,) = ax.plot(x, retrieved, c="C0", label="R") + line_meas = None + if measured is not None: + (line_meas,) = ax.plot(x, measured, c="C1", label="M") + line_truth = None + truth_curve = None if truth is None else _truth_spectrum(truth, axis) + if truth_curve is not None: + (line_truth,) = ax.plot(*truth_curve, c="k", lw=0.8, zorder=1, label="truth") + ax.set_xlabel(xlabel) + ax.set_ylabel("Power (a.u.)") + ax.set_ylim(0, 1.05) + ax.set_title("Spectrum") + xlim = _spectral_xlim(lam_min, lam_max, axis) + if xlim is not None: + ax.set_xlim(*xlim) + axp = ax.twinx() + (line_phase,) = axp.plot(x[pr.mask_w], pr.phi_w[pr.mask_w], c="C2") + # dashed polynomial fit of the spectral phase (GDD/TOD visualisation) + (line_phase_fit,) = axp.plot( + x[pr.mask_w], pr.phi_w_fit[pr.mask_w], "--", c="C4", alpha=0.7 + ) + axp.set_ylabel("Phase (rad)") + tphi = _truth_spectral_phase(pr.wavelength, truth) + line_truth_phase = None + if tphi is not None: + (line_truth_phase,) = axp.plot( + x[pr.mask_w], + tphi[pr.mask_w], + ":", + c="0.35", + lw=1.2, + label="truth phase", + ) + limits = _phase_ylim(pr.phi_w[pr.mask_w], None if tphi is None else tphi[pr.mask_w]) + if limits is not None: + axp.set_ylim(*limits) + text_dispersion = ax.text( + 0.05, 0.78, _dispersion_text(pr), transform=ax.transAxes, fontsize="small" + ) + edge, edge_color = _edge_text(pr) + text_edge = ax.text( + 0.05, 0.70, edge, transform=ax.transAxes, fontsize="small", color=edge_color + ) + # Upper right: the GDD/TOD and edge annotations occupy the upper left. + legend = _combined_legend(ax, axp, loc="upper right") + return SpectralArtists( + line_retr=line_retr, + line_phase=line_phase, + line_phase_fit=line_phase_fit, + ax_phase=axp, + legend=legend, + text_dispersion=text_dispersion, + text_edge=text_edge, + line_meas=line_meas, + line_truth=line_truth, + line_truth_phase=line_truth_phase, + ) + + +def plot_spectral( + ax, + pr: ProcessedResult, + *, + lam_min=None, + lam_max=None, + truth: TruthPulse | None = None, + axis: SpectralAxis = "frequency", +): + """Spectral intensity (left) and phase (right twin axis) against ``axis``. + + ``axis`` is ``"frequency"`` (PHz, the default: the space the retrieval works in, + where ``|E(ω)|²`` of a symmetric pulse is symmetric) or ``"wavelength"`` (nm, the + spectrometer's axis, carrying the 1/λ² density Jacobian). Both spectra are drawn as + unit-peak densities in the chosen variable; ``lam_min``/``lam_max`` (m) bound the + axis on either. The phase curves are the same on both. + + ``truth``, when it carries a spectrum, is overlaid as a thin black line under + the retrieved curve; when it carries a known spectral phase, that phase is + overlaid on the phase axis. It is drawn in the SAME gauge as the retrieved + phase (unwrapped, de-tilted, zeroed at the spectral peak — see + :func:`croak.processing.normalize_spectral_phase`), because spectral phase + is defined only up to a constant and a linear term; a truth pinned any other + way would show a tilt or offset that is gauge, not error. + + Returns the twin phase axes. :func:`draw_spectral` is the same panel for the + registry, returning every artist (:class:`SpectralArtists`). + """ + return _spectral_panel( + ax, pr, lam_min=lam_min, lam_max=lam_max, truth=truth, axis=axis + ).ax_phase + + +def plot_spectrogram( + ax, + t_centers, + wavelength, + S, + *, + lam_min=None, + lam_max=None, + halfwidth=None, + cmap=None, +): + """Plot a Gabor spectrogram (time fs × wavelength nm).""" + if cmap is None: + cmap = cmap_white("viridis") + m = ax.pcolormesh( + t_centers / 1e-15, + wavelength / 1e-9, + S, + cmap=cmap, + rasterized=True, + shading="auto", + ) + ax.set_xlabel("Time (fs)") + ax.set_ylabel("Wavelength (nm)") + ax.set_title("Spectrogram") + if lam_min and lam_max: + ax.set_ylim(lam_min / 1e-9, lam_max / 1e-9) + if halfwidth is not None: + ax.set_xlim(-halfwidth / 1e-15, halfwidth / 1e-15) + return m diff --git a/croak/plotting/retrieval.py b/croak/plotting/retrieval.py new file mode 100644 index 0000000..ffeec80 --- /dev/null +++ b/croak/plotting/retrieval.py @@ -0,0 +1,426 @@ +"""The retrieval summary: panel registry, pages, the overview and the page view. + +The twelve panels of :mod:`croak.plotting.panels` are registered in +:data:`RETRIEVAL_PANELS`; :func:`plot_retrieval` lays them out as the 3×4 +overview and :func:`plot_retrieval_page` draws one of the three 2×2 pages +(:data:`RETRIEVAL_PAGES`) the GUI shows as tabs. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from matplotlib.figure import Figure +from numpy.typing import ArrayLike + +from ..processing import ProcessedResult, TruthPulse +from ..result import RetrievalResult +from .artists import PanelArtists, ResidualArtists, TraceArtists +from .figure import _prepare_fig +from .panels import ( + RetrievalPlotData, + draw_convergence, + draw_delay_marginal, + draw_freq_marginal, + draw_measured_lin, + draw_measured_log, + draw_residual, + draw_retrieved_lin, + draw_retrieved_log, + draw_spectral, + draw_spectral_filter, + draw_spectrogram, + draw_temporal, + retrieval_plot_data, +) +from .pulse import SpectralAxis +from .updates import ( + _update_signature, + update_convergence, + update_delay_marginal, + update_freq_marginal, + update_measured_lin, + update_measured_log, + update_residual, + update_retrieved_lin, + update_retrieved_log, + update_spectral, + update_spectral_filter, + update_spectrogram, + update_temporal, +) + +if TYPE_CHECKING: + from matplotlib.axes import Axes + from matplotlib.collections import QuadMesh + + +type PanelDraw = Callable[[Axes, RetrievalPlotData], PanelArtists] +# Any: each update_ takes its own artists dataclass, and a registry entry +# typed with the union would reject them (parameters are contravariant). The draw +# side keeps the precise union because a return type is covariant. +type PanelUpdate = Callable[[Any, RetrievalPlotData], None] + + +@dataclass(frozen=True) +class PanelSpec: + """One retrieval panel: registry key, axes title, draw and in-place update.""" + + key: str + title: str + draw: PanelDraw + update: PanelUpdate + + +@dataclass(frozen=True) +class PageSpec: + """A figure's worth of panels laid out as a grid of registry keys. + + Attributes + ---------- + key, title : str + Identifier and human-readable name (the GUI's tab label). + mosaic : tuple of tuple of str + Rows of panel keys, as + :external+matplotlib:meth:`~matplotlib.figure.Figure.subplot_mosaic` + takes them. + """ + + key: str + title: str + mosaic: tuple[tuple[str, ...], ...] + + @property + def keys(self) -> tuple[str, ...]: + """The page's panel keys in reading order.""" + return tuple(key for row in self.mosaic for key in row) + + +@dataclass(frozen=True) +class ColorbarSpec: + """A colorbar shared by the listed panels (drawn only when all are present). + + A positive colorbar is always drawn; a second, ``neg_label``-ed one appears + when any of the panels holds negative samples (see :func:`signed_pcolormesh`). + """ + + panels: tuple[str, ...] + shrink: float = 0.6 + neg_label: str = "neg |·|" + + +#: The twelve retrieval panels, keyed, in the overview's reading order. +RETRIEVAL_PANELS: Mapping[str, PanelSpec] = { + spec.key: spec + for spec in ( + PanelSpec( + "measured_log", "Measured (log)", draw_measured_log, update_measured_log + ), + PanelSpec( + "retrieved_log", "Retrieved (log)", draw_retrieved_log, update_retrieved_log + ), + PanelSpec("temporal", "Retrieved pulse", draw_temporal, update_temporal), + PanelSpec( + "delay_marginal", + "Delay marginal", + draw_delay_marginal, + update_delay_marginal, + ), + PanelSpec( + "measured_lin", "Measured (lin)", draw_measured_lin, update_measured_lin + ), + PanelSpec( + "retrieved_lin", "Retrieved (lin)", draw_retrieved_lin, update_retrieved_lin + ), + PanelSpec("spectral", "Spectrum", draw_spectral, update_spectral), + PanelSpec( + "freq_marginal", + "Frequency marginal", + draw_freq_marginal, + update_freq_marginal, + ), + PanelSpec("convergence", "Convergence", draw_convergence, update_convergence), + PanelSpec("residual", "Residuals", draw_residual, update_residual), + PanelSpec( + "spectral_filter", + "Spectral filter", + draw_spectral_filter, + update_spectral_filter, + ), + PanelSpec("spectrogram", "Spectrogram", draw_spectrogram, update_spectrogram), + ) +} + +#: Colorbars: one per trace pair (they share a scale) and one for the residual. +RETRIEVAL_COLORBARS: tuple[ColorbarSpec, ...] = ( + ColorbarSpec(("measured_log", "retrieved_log")), + ColorbarSpec(("measured_lin", "retrieved_lin")), + ColorbarSpec(("residual",)), +) + +#: The three 2×2 pages the GUI shows as tabs. +RETRIEVAL_PAGES: Mapping[str, PageSpec] = { + page.key: page + for page in ( + PageSpec( + "traces", + "Traces", + (("measured_log", "retrieved_log"), ("measured_lin", "retrieved_lin")), + ), + # Each marginal sits under the domain it belongs to: delay under the + # time-domain pulse, frequency under the spectrum. + PageSpec( + "pulse", + "Pulse", + (("temporal", "spectral"), ("delay_marginal", "freq_marginal")), + ), + PageSpec( + "diagnostics", + "Diagnostics", + (("convergence", "residual"), ("spectral_filter", "spectrogram")), + ), + ) +} + +#: The 3×4 overview :func:`plot_retrieval` draws: traces left, pulse right (the +#: delay marginal beside the time-domain pulse, the frequency marginal beside the +#: spectrum), diagnostics along the bottom. +RETRIEVAL_OVERVIEW = PageSpec( + "overview", + "Retrieval overview", + ( + ("measured_log", "retrieved_log", "temporal", "delay_marginal"), + ("measured_lin", "retrieved_lin", "spectral", "freq_marginal"), + ("convergence", "residual", "spectral_filter", "spectrogram"), + ), +) + + +def _mappables(artists: PanelArtists) -> tuple[QuadMesh | None, QuadMesh | None]: + """Return the ``(positive, negative)`` images a colorbar can attach to.""" + if isinstance(artists, TraceArtists): + return artists.mesh_pos, artists.mesh_neg + if isinstance(artists, ResidualArtists): + return artists.mesh, None + return None, None + + +def _draw_colorbars( + fig, axd: Mapping[str, Axes], artists: Mapping[str, PanelArtists] +) -> None: + """Add every :data:`RETRIEVAL_COLORBARS` entry whose panels are all in ``axd``.""" + for spec in RETRIEVAL_COLORBARS: + if not all(key in axd for key in spec.panels): + continue + axes = [axd[key] for key in spec.panels] + pairs = [_mappables(artists[key]) for key in spec.panels] + pos = next((p for p, _ in pairs if p is not None), None) + if pos is None: + continue + # Both panels of a pair share vmin/vmax and cmap, so either can seed the bar. + fig.colorbar(pos, ax=axes, shrink=spec.shrink) + neg = next((n for _, n in pairs if n is not None), None) + if neg is not None: + fig.colorbar(neg, ax=axes, shrink=spec.shrink, label=spec.neg_label) + + +def _draw_panels( + fig, page: PageSpec, data: RetrievalPlotData +) -> tuple[dict[str, Axes], dict[str, PanelArtists]]: + """Lay ``page``'s mosaic out in ``fig``, draw every panel and its colourbars.""" + axd = fig.subplot_mosaic([list(row) for row in page.mosaic]) + artists = {key: RETRIEVAL_PANELS[key].draw(ax, data) for key, ax in axd.items()} + _draw_colorbars(fig, axd, artists) + return axd, artists + + +class RetrievalPageView: + """Maintain a page of retrieval plots for repeated GUI updates. + + The live preview hands the Retrieve stage a new result a few times a second. + Clearing a figure and rebuilding a page's axes, colourbars and legends for + each one costs a full constrained-layout solve and lets legends jump between + frames; this view draws the page once and then pushes later results onto the + existing artists when :meth:`can_update` accepts the new data, as + :class:`FrogFilterView` does for the preprocessing figure. It redraws when + the grid, optional curves or panel structure change. + + Parameters + ---------- + page : PageSpec + Panels and their layout. + + Examples + -------- + >>> view = RetrievalPageView(RETRIEVAL_PAGES["pulse"]) # doctest: +SKIP + >>> view.draw(fig, data1) # doctest: +SKIP + >>> if view.can_update(data2): # doctest: +SKIP + ... view.update(data2) + ... else: + ... view.draw(fig, data2) + """ + + def __init__(self, page: PageSpec): + self.page = page + self.fig: Figure | None = None + self.axd: dict[str, Axes] = {} + self.artists: dict[str, PanelArtists] = {} + self._signature: tuple | None = None + + @property + def attached(self) -> bool: + """Whether the view has drawn into a figure.""" + return self.fig is not None + + def draw(self, fig: Figure, data: RetrievalPlotData) -> None: + """Clear ``fig`` and draw the page afresh for ``data``. + + Constrained layout is solved on the first real draw and then frozen (the + engine is swapped for matplotlib's placeholder), so later in-place updates + neither re-solve it nor nudge the panels as tick labels change width. + """ + _prepare_fig(fig, None) + self.fig = fig + self.axd, self.artists = _draw_panels(fig, self.page, data) + self._signature = _update_signature(data) + canvas = fig.canvas + + def freeze(_event) -> None: + canvas.mpl_disconnect(cid) + if self.fig is fig: + fig.set_layout_engine("none") + + cid = canvas.mpl_connect("draw_event", freeze) + + def can_update(self, data: RetrievalPlotData) -> bool: + """Whether ``data`` differs from the drawn result in values only.""" + return self.attached and _update_signature(data) == self._signature + + def update(self, data: RetrievalPlotData) -> None: + """Push ``data`` onto the drawn artists. + + Raises + ------ + RuntimeError + If nothing has been drawn yet. + ValueError + If :meth:`can_update` is false (different grid or optional artists); + :meth:`draw` it instead. + """ + if not self.attached: + raise RuntimeError("draw(fig, data) before update(data)") + if not self.can_update(data): + raise ValueError("the result's shape or contents changed: draw() it") + for key, artists in self.artists.items(): + RETRIEVAL_PANELS[key].update(artists, data) + + def reset(self) -> None: + """Forget the figure (someone else cleared it); the next call must draw.""" + self.fig = None + self.axd = {} + self.artists = {} + self._signature = None + + +def plot_retrieval_page( + data: RetrievalPlotData, + page: PageSpec, + *, + figsize: tuple[float, float] = (8.5, 6.3), + fig: Figure | None = None, +) -> Figure: + """Draw one page of retrieval panels (a :data:`RETRIEVAL_PAGES` entry). + + The default ``figsize`` gives a 2×2 page the same per-panel size as the + overview's default 17.0×9.45 in. + + Parameters + ---------- + data : RetrievalPlotData + From :func:`retrieval_plot_data`. + page : PageSpec + Which panels, in which grid; :data:`RETRIEVAL_OVERVIEW` draws all twelve. + figsize : tuple of float + Size (in) of a new figure; ignored when ``fig`` is given. + fig : Figure, optional + Existing figure to clear and draw into (a GUI canvas). + + Examples + -------- + >>> data = retrieval_plot_data(result, measured=trace) # doctest: +SKIP + >>> fig = plot_retrieval_page(data, RETRIEVAL_PAGES["pulse"]) # doctest: +SKIP + """ + fig = _prepare_fig(fig, figsize) + _draw_panels(fig, page, data) + return fig + + +def plot_retrieval( + result: RetrievalResult, + *, + measured: ArrayLike | None = None, + Iomega_meas: ArrayLike | None = None, + lam_min: float | None = None, + lam_max: float | None = None, + flim: tuple[float, float] | None = None, + tracedb: float = 30.0, + cmap: str = "viridis", + truth: TruthPulse | None = None, + energy: float | None = None, + figsize=(17.0, 9.45), + fig: Figure | None = None, + processed: ProcessedResult | None = None, + spectral_axis: SpectralAxis = "frequency", +) -> Figure: + """12-panel retrieval summary: the standard at-a-glance quality check. + + The panels are :data:`RETRIEVAL_PANELS` in the :data:`RETRIEVAL_OVERVIEW` + layout — measured/retrieved traces (log and linear) on the left, the pulse, + spectrum and marginals on the right, convergence, residual, spectral filter + and spectrogram along the bottom. :func:`plot_retrieval_page` draws a subset. + + ``lam_min``/``lam_max`` (m) bound the **wavelength** axes — the spectrum and + spectrogram panels. ``flim`` bounds the **frequency** axis of the trace and + residual panels, as ``(f_min, f_max)`` in Hz; the default shows the whole + retrieval grid. The two are separate because the trace sits at the *signal* + frequency, which for SHG is twice the pulse's. Reach for ``flim`` when the + retrieval grid is much wider than the signal — typical when retrieving + directly on a synthesis grid rather than on one cropped by + :func:`~croak.preprocess.load_and_clean` — where the trace would otherwise be + a thin stripe in a mostly empty axis. + + ``energy`` is an optional measured pulse energy (J); when given, the temporal + panel is shown in absolute power (see :func:`~croak.processing.process_result`). + + ``processed`` is an optional, already-computed + :class:`~croak.processing.ProcessedResult` **for this same result**: pass it when + the caller needs the processed quantities too (the GUI shows them in a table + beside the figure) so they are not derived twice. It must have been built with + the same ``measured``/``Iomega_meas``/``energy``, which are then used only for + the trace panels; a mismatched one raises :class:`ValueError`. + + ``spectral_axis`` draws the spectrum panel against ``"frequency"`` (PHz, the + default) or ``"wavelength"`` (nm); see :func:`plot_spectral`. + + Raises + ------ + ValueError + If ``result`` carries no trace, or ``processed`` belongs to another result. + """ + data = retrieval_plot_data( + result, + measured=measured, + Iomega_meas=Iomega_meas, + lam_min=lam_min, + lam_max=lam_max, + flim=flim, + tracedb=tracedb, + cmap=cmap, + truth=truth, + energy=energy, + processed=processed, + spectral_axis=spectral_axis, + ) + return plot_retrieval_page(data, RETRIEVAL_OVERVIEW, figsize=figsize, fig=fig) diff --git a/croak/plotting/scale.py b/croak/plotting/scale.py new file mode 100644 index 0000000..c4a4a71 --- /dev/null +++ b/croak/plotting/scale.py @@ -0,0 +1,152 @@ +"""On-screen text density of a figure squeezed below its design size.""" + +from __future__ import annotations + +from dataclasses import dataclass + +# --------------------------------------------------------------------------- +# On-screen density +# --------------------------------------------------------------------------- +# Matplotlib sizes text in points, so when a figure is squeezed into a small window +# the axes shrink and the text does not: on a 1366x768 laptop the twelve-panel +# retrieval summary spent 78 % of its area on titles, labels and padding. The GUI +# canvas (croak.gui.canvas.MplCanvas) therefore scales the rc font sizes by how far +# the canvas has fallen below the size its figure was designed for. The scale is a +# pure function of geometry, kept here so the plotting layer stays Qt-free and the +# thresholds are testable headlessly. + +#: Smallest fraction of the design font size that text may shrink to. Below ~70 % +#: a 10 pt label is 7 pt, the limit of comfortable legibility on a laptop panel. +FONT_SCALE_MIN = 0.7 +#: Quantisation step of :func:`plot_scale`. A window resize re-plots only when the +#: scale crosses a step, so a few pixels of drag never trigger a redraw. +FONT_SCALE_STEP = 0.05 +#: Below this scale :attr:`PlotScale.compact` is set, for plotting code that wants +#: to drop optional decoration (a second legend entry, a colorbar label) when tight. +COMPACT_BELOW = 0.9 + + +@dataclass(frozen=True) +class PlotScale: + """Text density of a figure relative to the size its layout was designed for. + + Parameters + ---------- + font : float + Multiplier applied to every rc font size (and text padding), in + ``[FONT_SCALE_MIN, 1.0]``. ``1.0`` is matplotlib's default 10 pt base. + + Examples + -------- + >>> PlotScale(0.85).compact + True + >>> FULL_SCALE.compact + False + """ + + font: float = 1.0 + + @property + def compact(self) -> bool: + """Whether the figure is tight enough to drop optional decoration.""" + return self.font < COMPACT_BELOW + + +#: The unscaled density: matplotlib's defaults. +FULL_SCALE = PlotScale() + + +def plot_scale( + width_in: float, height_in: float, design_in: tuple[float, float] +) -> PlotScale: + """Return the text density for a figure of the given size. + + The scale is the fraction of the design size that fits in the *tighter* + direction, ``min(width / design_width, height / design_height)``, quantised + to :data:`FONT_SCALE_STEP` and clamped to ``[FONT_SCALE_MIN, 1.0]``. A figure + at or above its design size is drawn at full scale; one squeezed to 60 % of + its design height gets the minimum. + + Parameters + ---------- + width_in, height_in : float + Current figure size in inches (``Figure.get_size_inches()``). On a Qt + canvas this is the logical pixel size / 100 on every display, because + matplotlib folds the device-pixel ratio into ``figure.dpi``. + design_in : tuple of float + ``(width, height)`` in inches the figure's layout was designed for — the + ``figsize`` a stage passes to its canvas. + + Returns + ------- + PlotScale + + Raises + ------ + ValueError + If any size is not positive. + + Examples + -------- + >>> plot_scale(9.91, 4.21, (12.0, 7.0)).font # Retrieve page at 1366x768 + 0.7 + >>> plot_scale(15.45, 7.33, (12.0, 7.0)).font # ... at 1920x1080 + 1.0 + """ + design_w, design_h = design_in + if min(width_in, height_in, design_w, design_h) <= 0: + raise ValueError( + f"figure and design sizes must be positive, got {width_in}x{height_in} " + f"in for a {design_w}x{design_h} in design" + ) + ratio = min(width_in / design_w, height_in / design_h) + # Quantise to the nearest step; round to two decimals so equal steps compare + # equal as floats (0.85, not 0.8500000000000001). + quantised = round(round(ratio / FONT_SCALE_STEP) * FONT_SCALE_STEP, 2) + return PlotScale(font=min(1.0, max(FONT_SCALE_MIN, quantised))) + + +# Matplotlib's default sizes spelled out in points. The rc defaults for the title +# and label keys are the *relative* strings 'large'/'medium', which cannot be +# multiplied; writing the resolved points also makes the scaling immune to a user +# matplotlibrc with absolute sizes. The four pads are included because text +# *padding* is a fixed-pixel cost too. +_SCALED_RC_BASE: dict[str, float] = { + "font.size": 10.0, + "axes.titlesize": 12.0, + "axes.labelsize": 10.0, + "xtick.labelsize": 10.0, + "ytick.labelsize": 10.0, + "legend.fontsize": 10.0, + "figure.titlesize": 12.0, + "axes.labelpad": 4.0, + "axes.titlepad": 6.0, + "xtick.major.pad": 3.5, + "ytick.major.pad": 3.5, +} + + +def scaled_rc(scale: PlotScale) -> dict[str, float]: + """Return rcParams overrides that shrink every font size and pad by ``scale``. + + Use with :external+matplotlib:func:`matplotlib.rc_context` *around the plotting + call*: matplotlib resolves relative sizes (``fontsize="small"``, the ``'large'`` + title default) against ``font.size`` when an artist is created, not when it is + drawn, so the context must enclose everything that creates text. + + Parameters + ---------- + scale : PlotScale + The density; :data:`FULL_SCALE` reproduces matplotlib's defaults. + + Returns + ------- + dict + ``{rc key: value in points}`` for the font-size and padding keys. + + Examples + -------- + >>> round(scaled_rc(PlotScale(0.7))["axes.titlesize"], 2) + 8.4 + """ + return {key: base * scale.font for key, base in _SCALED_RC_BASE.items()} diff --git a/croak/plotting/traces.py b/croak/plotting/traces.py new file mode 100644 index 0000000..e3b7dfa --- /dev/null +++ b/croak/plotting/traces.py @@ -0,0 +1,570 @@ +"""FROG-trace images: colormaps, signed images, residual, marginals and composites. + +Custom colormaps (white at the low end; a red band for negatives), the +sign-splitting :func:`signed_pcolormesh`, the residual and marginal panels, and +the trace composites :func:`plot_frog_filter` (with its artist-reusing twin +:class:`FrogFilterView`) and :func:`plot_simulated_trace`. +""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING + +import numpy as np +from matplotlib.colors import ListedColormap +from matplotlib.figure import Figure + +from ..processing import marginals +from .artists import MarginalArtists +from .figure import _prepare_fig +from .units import _TWOPI_PHZ + +if TYPE_CHECKING: + from numpy.typing import NDArray + + +# --------------------------------------------------------------------------- +# Colormaps +# --------------------------------------------------------------------------- +def cmap_white(base: str = "viridis", *, n: int = 8, white: bool = True, N: int = 512): + """Build a base colormap with (optionally) a white point at the low end.""" + import matplotlib as mpl + + cm = mpl.colormaps[base] + vals = np.linspace(0, 1, n) + cols = cm(vals) + if white: + cols[0] = [1, 1, 1, 1] + xi = np.linspace(0, 1, N) + out = np.empty((N, 4)) + for k in range(4): + out[:, k] = np.interp(xi, vals, cols[:, k]) + return ListedColormap(out) + + +def cmap_negwhite( + base: str = "viridis", + *, + n: int = 8, + white: bool = True, + negfrac: float = 0.0, + N: int = 512, +): + """Like :func:`cmap_white` but with a red gradient for negative values.""" + if negfrac <= 0: + return cmap_white(base, n=n, white=white, N=N) + n_pos = max(round(N * (1 - negfrac)), 2) + n_neg = N - n_pos + pos = cmap_white(base, n=n, white=white, N=n_pos)(np.linspace(0, 1, n_pos)) + neg = np.empty((n_neg, 4)) + for i in range(n_neg): + t = i / max(n_neg - 1, 1) + neg[i] = [1.0, t, t, 1.0] + return ListedColormap(np.vstack([neg, pos])) + + +def _white_red(): + """White→red colormap used to render negative trace values by magnitude.""" + from matplotlib.colors import LinearSegmentedColormap + + return LinearSegmentedColormap.from_list("white_red", ["white", "red"]) + + +def _signed_magnitude( + C, *, db: bool, tracedb: float, vmax: float | None +) -> tuple[NDArray[np.float64], NDArray[np.bool_], float, float]: + """Split ``C`` into magnitude, negative mask and the shared ``(vmin, vmax)``. + + One code path for drawing and for updating in place. + """ + C = np.asarray(C, dtype=float) + neg = C < 0 + if db: + mag = np.clip(10 * np.log10(np.maximum(np.abs(C), 1e-30)), -tracedb, 0.0) + return mag, neg, -tracedb, 0.0 + mag = np.abs(C) + return mag, neg, 0.0, float(np.nanmax(mag)) if vmax is None else vmax + + +def signed_pcolormesh(ax, x, y, C, *, db, tracedb=30.0, cmap_pos, vmax=None): + """Pcolormesh that splits an image's sign onto two colormaps. + + Positive samples are drawn with ``cmap_pos`` (e.g. viridis); negative + samples are drawn by *magnitude* with a white→red map. Both maps share the + **same** scale (``[0, vmax]`` linear, ``[-tracedb, 0]`` dB), so a negative is + only as saturated as a positive of equal magnitude — small negatives stay + faint instead of dominating — and zero is white in both (pair ``cmap_pos`` + with :func:`cmap_white`). ``C`` follows the usual + :external+matplotlib:meth:`~matplotlib.axes.Axes.pcolormesh` convention — + shape ``(len(y), len(x))`` (a FROG ``(Nλ, Nτ)`` array already has this shape + for x = delay and y = wavelength). + + Returns ``(mappable_pos, mappable_neg)`` (the second is ``None`` when there + are no negative samples). + """ + mag, neg, vmin, vhi = _signed_magnitude(C, db=db, tracedb=tracedb, vmax=vmax) + mpos = ax.pcolormesh( + x, + y, + np.ma.masked_where(neg, mag), + vmin=vmin, + vmax=vhi, + cmap=cmap_pos, + rasterized=True, + shading="auto", + ) + mneg = None + if neg.any(): + mneg = ax.pcolormesh( + x, + y, + np.ma.masked_where(~neg, mag), + vmin=vmin, + vmax=vhi, + cmap=_white_red(), + rasterized=True, + shading="auto", + ) + return mpos, mneg + + +def plot_residual(ax, omega, delays, resid, omega0, *, halfwidth=None, flim=None): + """Plot the measured−retrieved residual with a symmetric blue-white-red map. + + ``flim`` optionally bounds the frequency axis, as ``(f_min, f_max)`` in Hz. + """ + mr = float(np.max(np.abs(resid))) or 1.0 + freq = (np.asarray(omega) + omega0) / _TWOPI_PHZ + m = ax.pcolormesh( + np.asarray(delays) / 1e-15, + freq, + resid, + vmin=-mr, + vmax=mr, + cmap=cmap_white("bwr", n=32, white=False), + rasterized=True, + shading="auto", + ) + ax.set_xlabel("Delay (fs)") + ax.set_ylabel("Frequency (PHz)") + ax.set_title("Residuals") + if halfwidth is not None: + ax.set_xlim(-halfwidth / 1e-15, halfwidth / 1e-15) + if flim is not None: + ax.set_ylim(flim[0] / 1e15, flim[1] / 1e15) + return m + + +def plot_marginal( + ax, x, retrieved, measured=None, *, xlabel="", scale=1.0, halfwidth=None +) -> MarginalArtists: + """Plot a (retrieved vs measured) trace marginal.""" + (line_retr,) = ax.plot(np.asarray(x) / scale, retrieved, c="C0", label="R") + line_meas = None + if measured is not None: + (line_meas,) = ax.plot(np.asarray(x) / scale, measured, c="C1", label="M") + ax.set_xlabel(xlabel) + ax.set_ylabel("Marginal (a.u.)") + ax.set_ylim(0, 1.05) + if halfwidth is not None: + ax.set_xlim(-halfwidth / scale, halfwidth / scale) + legend = ax.legend( + loc="upper left", handlelength=1.0, frameon=False, fontsize="small" + ) + return MarginalArtists(line_retr=line_retr, legend=legend, line_meas=line_meas) + + +def filter_view_vmax(td) -> float: + """Return the shared positive colour scale for the six-panel filter view. + + Take the largest **positive** sample across the measured, filtered and + regridded traces, restricting the two wavelength-domain arrays to the + measurement window ``td.lamm_lims`` when set. + + Both restrictions matter on real data. An absolute-calibration curve + amplifies the background-subtracted noise far outside the signal band, so + scaling by ``max|·|`` lets a large *negative* excursion set the *positive* + scale: on one of our measured deep-UV traces, whose signal peaks at ``+1`` + but dips to ``-5.7`` at 1097 nm, every linear panel rendered its signal at + 18 % of the colour range. + + The three panels share this one number so the row's single colourbar means + something; each array is peak-normalised upstream, so they are comparable. + If no positive sample remains, fall back to the largest absolute value, + then 1.0, so the returned scale is always positive. + """ + + def _positive_max(arr, lam=None) -> float: + a = np.asarray(arr, dtype=float) + if lam is not None and td.lamm_lims is not None: + lo, hi = td.lamm_lims + band = (np.asarray(lam, dtype=float) >= lo) & ( + np.asarray(lam, dtype=float) <= hi + ) + if band.any(): + a = a[band] + hi = float(np.nanmax(a)) if a.size else 0.0 + return hi if hi > 0.0 else 0.0 + + vmax = max( + _positive_max(td.Ifrog_meas, td.lam_frog), + _positive_max(td.Ifrog_filt, td.lam_frog), + _positive_max(td.trace), + ) + if vmax > 0.0: + return vmax + # Degenerate (all-negative or all-zero) trace: keep a usable, positive scale. + fallback = max( + float(np.nanmax(np.abs(td.Ifrog_meas))), + float(np.nanmax(np.abs(td.Ifrog_filt))), + float(np.nanmax(np.abs(td.trace))), + ) + return fallback if fallback > 0.0 else 1.0 + + +def plot_frog_filter( + td, + *, + tracedb: float = 30.0, + cmap: str = "viridis", + figsize=(9.45, 4.72), + fig: Figure | None = None, +) -> Figure: + """6-panel measured/filtered (wavelength-domain) + regridded (frequency-domain). + + Positive values use the viridis (white-low) map; negative values — kept + through filtering rather than clamped — are drawn by magnitude on a + white→red map so they are visible and not mistaken for signal. Each row + (the dB row and the linear row) carries one positive + one negative + colorbar on the right-hand side. + """ + fig = _prepare_fig(fig, figsize) + axd = fig.subplot_mosaic("abe\ncdf") + cm = cmap_white(cmap) + # The linear row shares a single scale so its one colorbar is meaningful. + lin_vmax = filter_view_vmax(td) + + def _panel(ax, x, y, C, *, db, title, xlabel): + mpos, mneg = signed_pcolormesh( + ax, + x, + y, + C, + db=db, + tracedb=tracedb, + cmap_pos=cm, + vmax=None if db else lin_vmax, + ) + ax.set_xlabel(xlabel) + ax.set_ylabel("Delay (fs)") + ax.set_title(title) + return mpos, mneg + + def _row_cbars(axes, mpos, negs): + """One positive + (if any) one negative colorbar at the row's right.""" + fig.colorbar(mpos, ax=axes, shrink=0.5) + mneg = next((m for m in negs if m is not None), None) + if mneg is not None: + fig.colorbar(mneg, ax=axes, shrink=0.5, label="neg |·|") + + lam_nm = td.lam_frog / 1e-9 + freq = (td.omega + td.omega0_trace) / _TWOPI_PHZ + WL = "Wavelength (nm)" + HZ = "Frequency (PHz)" + + # top row: dB (log) — measured, filtered, regridded + pa, na = _panel( + axd["a"], + lam_nm, + td.tau_meas / 1e-15, + td.Ifrog_meas.T, + db=True, + title="Measured (dB)", + xlabel=WL, + ) + _pb, nb = _panel( + axd["b"], + lam_nm, + td.tau_filt / 1e-15, + td.Ifrog_filt.T, + db=True, + title="Filtered (dB)", + xlabel=WL, + ) + _pe, ne = _panel( + axd["e"], + freq, + td.delays / 1e-15, + td.trace.T, + db=True, + title="Regridded (dB)", + xlabel=HZ, + ) + _row_cbars([axd["a"], axd["b"], axd["e"]], pa, (nb, ne, na)) + + # bottom row: linear — measured, filtered, regridded + pc, nc = _panel( + axd["c"], + lam_nm, + td.tau_meas / 1e-15, + td.Ifrog_meas.T, + db=False, + title="Measured (lin)", + xlabel=WL, + ) + _pd, nd = _panel( + axd["d"], + lam_nm, + td.tau_filt / 1e-15, + td.Ifrog_filt.T, + db=False, + title="Filtered (lin)", + xlabel=WL, + ) + _pf, nf = _panel( + axd["f"], + freq, + td.delays / 1e-15, + td.trace.T, + db=False, + title="Regridded (lin)", + xlabel=HZ, + ) + _row_cbars([axd["c"], axd["d"], axd["f"]], pc, (nd, nf, nc)) + return fig + + +def _decimate_for_display(C, x, y, *, cap=1000): + """Stride a ``(ny, nx)`` image and its axes down to at most ``cap`` per side. + + The filter figure panels are only a few hundred screen pixels wide, so + rasterising a multi-thousand-sample trace is wasted work. Plain strided + subsampling is enough for a diagnostic preview (no anti-alias needed — the + physics-correct arrays are kept in ``td``; this only thins what is drawn). + """ + ny, nx = C.shape + sy = max(ny // cap, 1) + sx = max(nx // cap, 1) + if sy == 1 and sx == 1: + return C, x, y + return C[::sy, ::sx], x[::sx], y[::sy] + + +def _resample_uniform_x(x, C): + """Resample ``C`` (``(ny, nx)``) along axis 1 onto a uniform ``x`` grid. + + :external+matplotlib:class:`~matplotlib.image.AxesImage` (``imshow``) assumes evenly + spaced pixels and is positioned by a single linear ``extent``. A *wavelength* axis + derived from a uniform **frequency** grid (the simulated-trace case) is strongly + non-uniform, so a feature would otherwise be painted at the wrong wavelength (e.g. a + 260 nm signal drawn near 480 nm over a 150–800 nm span). Linearly interpolating each + delay row onto an evenly spaced ``x`` restores the correct positions; an + already-uniform axis (the frequency panels, or a uniform-λ spectrometer trace) is + returned unchanged. ``x`` must be ascending. + """ + x = np.asarray(x, dtype=float) + if x.size < 3: + return x, C + d = np.diff(x) + if np.allclose(d, d[0], rtol=1e-3, atol=0.0): + return x, C # already uniform — no interpolation needed + xu = np.linspace(x[0], x[-1], x.size) + Cu = np.empty_like(C) + for i in range(C.shape[0]): + Cu[i] = np.interp(xu, x, C[i]) + return xu, Cu + + +class FrogFilterView: + """Maintain a preprocessing figure for repeated GUI updates. + + The artist-reusing twin of :func:`plot_frog_filter`, which clears and + rebuilds the whole figure (6 panels × up to 2 ``pcolormesh`` + 4 + colourbars) on every call: ~150–400 ms, the dominant cost of the GUI + preprocessing redraw. Call :meth:`attach` once to create the panels, images + and colourbars, then :meth:`update` to push new data via + ``set_data``/``set_extent``/``set_clim`` onto the persistent images, which + benchmarks ~4× faster. Positive and negative samples use the same + signed-magnitude convention as :func:`signed_pcolormesh`. Non-uniform + wavelength arrays are interpolated onto a uniform display grid; the + underlying :class:`croak.preprocess.TraceData` arrays are unchanged. + """ + + _PANELS = ( + # key, title, xlabel-kind ("wl"/"hz"), db + ("a", "Measured (dB)", "wl", True), + ("b", "Filtered (dB)", "wl", True), + ("e", "Regridded (dB)", "hz", True), + ("c", "Measured (lin)", "wl", False), + ("d", "Filtered (lin)", "wl", False), + ("f", "Regridded (lin)", "hz", False), + ) + + def __init__( + self, *, tracedb: float = 30.0, cmap: str = "viridis", cap: int = 1000 + ): + self.tracedb = tracedb + self.cap = cap + self._cmap_pos = cmap_white(cmap) + self._cmap_pos.set_bad(alpha=0.0) # masked (negative) pixels stay clear + self._cmap_neg = _white_red() + self._cmap_neg.set_bad(alpha=0.0) + self.fig = None + self.axd = None + self.images: dict[str, tuple] = {} + + @property + def attached(self) -> bool: + """Whether a figure is currently attached to this view.""" + return self.fig is not None + + def attach(self, fig: Figure) -> None: + """Build the mosaic, images and colourbars once into ``fig``.""" + self.fig = fig + fig.clear() + with contextlib.suppress(Exception): + fig.set_layout_engine("constrained") + self.axd = fig.subplot_mosaic("abe\ncdf") + empty = np.zeros((1, 1)) + for key, title, kind, _db in self._PANELS: + ax = self.axd[key] + pos = ax.imshow( + empty, + origin="lower", + aspect="auto", + cmap=self._cmap_pos, + interpolation="nearest", + ) + neg = ax.imshow( + empty, + origin="lower", + aspect="auto", + cmap=self._cmap_neg, + interpolation="nearest", + ) + self.images[key] = (pos, neg) + ax.set_title(title) + ax.set_xlabel("Wavelength (nm)" if kind == "wl" else "Frequency (PHz)") + ax.set_ylabel("Delay (fs)") + # One positive + one negative colorbar per row (dB row a/b/e, lin row c/d/f). + self._cb_top = fig.colorbar( + self.images["a"][0], ax=[self.axd[k] for k in "abe"], shrink=0.5 + ) + self._cb_top_neg = fig.colorbar( + self.images["a"][1], + ax=[self.axd[k] for k in "abe"], + shrink=0.5, + label="neg |·|", + ) + self._cb_bot = fig.colorbar( + self.images["c"][0], ax=[self.axd[k] for k in "cdf"], shrink=0.5 + ) + self._cb_bot_neg = fig.colorbar( + self.images["c"][1], + ax=[self.axd[k] for k in "cdf"], + shrink=0.5, + label="neg |·|", + ) + + def _set_panel(self, key, x, y, C, *, db, vmax=None): + if self.axd is None: + raise RuntimeError("live plot is not attached to a figure") + pos_im, neg_im = self.images[key] + C = np.asarray(C, dtype=float) + C, x, y = _decimate_for_display(C, np.asarray(x), np.asarray(y), cap=self.cap) + # imshow paints evenly spaced pixels; a wavelength axis built from a + # uniform frequency grid (simulated traces) is non-uniform, so resample + # onto an even axis to place features at the right wavelength. + x, C = _resample_uniform_x(x, C) + neg = C < 0 + if db: + mag = np.clip( + 10 * np.log10(np.maximum(np.abs(C), 1e-30)), -self.tracedb, 0.0 + ) + vmin, vhi = -self.tracedb, 0.0 + else: + mag = np.abs(C) + vmin = 0.0 + vhi = float(np.nanmax(mag)) if vmax is None else vmax + extent = (float(x[0]), float(x[-1]), float(y[0]), float(y[-1])) + for im, masked in ( + (pos_im, np.ma.masked_where(neg, mag)), + (neg_im, np.ma.masked_where(~neg, mag)), + ): + im.set_data(masked) + im.set_clim(vmin, vhi) + im.set_extent(extent) + ax = self.axd[key] + ax.set_xlim(extent[0], extent[1]) + ax.set_ylim(extent[2], extent[3]) + + def update(self, td) -> None: + """Push a new :class:`~croak.preprocess.TraceData` onto the existing artists.""" + if not self.attached: + raise RuntimeError("attach(fig) before update(td)") + lam_nm = td.lam_frog / 1e-9 + freq = (td.omega + td.omega0_trace) / _TWOPI_PHZ + lin_vmax = filter_view_vmax(td) + self._set_panel("a", lam_nm, td.tau_meas / 1e-15, td.Ifrog_meas.T, db=True) + self._set_panel("b", lam_nm, td.tau_filt / 1e-15, td.Ifrog_filt.T, db=True) + self._set_panel("e", freq, td.delays / 1e-15, td.trace.T, db=True) + self._set_panel( + "c", lam_nm, td.tau_meas / 1e-15, td.Ifrog_meas.T, db=False, vmax=lin_vmax + ) + self._set_panel( + "d", lam_nm, td.tau_filt / 1e-15, td.Ifrog_filt.T, db=False, vmax=lin_vmax + ) + self._set_panel( + "f", freq, td.delays / 1e-15, td.trace.T, db=False, vmax=lin_vmax + ) + # The linear row's scale changes with the data; refresh its colorbars. + self._cb_bot.update_normal(self.images["c"][0]) + self._cb_bot_neg.update_normal(self.images["c"][1]) + + +def plot_simulated_trace( + omega, + delays, + trace, + omega0_trace, + *, + tracedb: float = 40.0, + cmap: str = "viridis", + figsize=(7.87, 5.51), + fig: Figure | None = None, +) -> Figure: + """4-panel view of a simulated trace: dB, linear, and the two marginals.""" + trace = np.asarray(trace, dtype=float) + fig = _prepare_fig(fig, figsize) + axd = fig.subplot_mosaic("ab\ncd") + cm = cmap_white(cmap) + freq = (omega + omega0_trace) / _TWOPI_PHZ + + def _panel(ax, *, db, title): + mpos, mneg = signed_pcolormesh( + ax, delays / 1e-15, freq, trace, db=db, tracedb=tracedb, cmap_pos=cm + ) + ax.set_xlabel("Delay (fs)") + ax.set_ylabel("Frequency (PHz)") + ax.set_title(title) + return mpos, mneg + + ma, na = _panel(axd["a"], db=True, title="FROG (dB)") + mb, nb = _panel(axd["b"], db=False, title="FROG (lin)") + fig.colorbar(ma, ax=[axd["a"]], shrink=0.6) + if na is not None: + fig.colorbar(na, ax=[axd["a"]], shrink=0.6, label="neg |·|") + fig.colorbar(mb, ax=[axd["b"]], shrink=0.6) + if nb is not None: + fig.colorbar(nb, ax=[axd["b"]], shrink=0.6, label="neg |·|") + wmarg, tmarg = marginals(trace) + axd["c"].plot(delays / 1e-15, tmarg, c="C0") + axd["c"].set_xlabel("Delay (fs)") + axd["c"].set_ylabel("Marginal") + axd["c"].set_ylim(0, 1.05) + axd["d"].plot((omega + omega0_trace) / _TWOPI_PHZ, wmarg, c="C0") + axd["d"].set_xlabel("Frequency (PHz)") + axd["d"].set_ylabel("Marginal") + axd["d"].set_ylim(0, 1.05) + return fig diff --git a/croak/plotting/uncertainty.py b/croak/plotting/uncertainty.py new file mode 100644 index 0000000..8646b17 --- /dev/null +++ b/croak/plotting/uncertainty.py @@ -0,0 +1,233 @@ +"""Uncertainty figures: FWHM distributions, intensity bands, thickness systematic.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from matplotlib.figure import Figure + +from .figure import _prepare_fig + +if TYPE_CHECKING: + from ..uncertainty import UncertaintyResult + + +# --------------------------------------------------------------------------- +# Uncertainty +# --------------------------------------------------------------------------- +def plot_fwhm_distribution( + ax, uresult: UncertaintyResult, *, truth: float | None = None +) -> None: + """Histogram of bootstrap FWHM samples with the point estimate and intervals. + + Parameters + ---------- + ax : matplotlib.axes.Axes + Axis to draw into. + uresult : UncertaintyResult + The bootstrap result (its ``samples`` are histogrammed). + truth : float, optional + A known true FWHM (s) to mark with a dashed line (synthetic tests). + """ + samples = np.asarray(uresult.samples, dtype=float) / 1e-15 + nbins = int(np.clip(samples.size // 5, 12, 40)) + ax.hist(samples, bins=nbins, color="C0", alpha=0.7, edgecolor="white", lw=0.4) + lo95, hi95 = uresult.interval_95 + lo68, hi68 = uresult.interval_68 + ax.axvspan(lo95 / 1e-15, hi95 / 1e-15, color="C0", alpha=0.10, label="95 %") + ax.axvspan(lo68 / 1e-15, hi68 / 1e-15, color="C0", alpha=0.20, label="68 %") + ax.axvline(uresult.point_estimate / 1e-15, c="C3", lw=1.5, label="estimate") + if truth is not None: + ax.axvline(truth / 1e-15, c="0.35", ls="--", lw=1.2, label="truth") + ax.set_xlabel("FWHM (fs)") + ax.set_ylabel("Replicates") + ax.set_title( + f"{uresult.point_estimate / 1e-15:.2f} ± {uresult.plus_minus / 1e-15:.2f} fs" + f" (B={uresult.n_converged})" + ) + ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") + + +def plot_intensity_band(ax, uresult: UncertaintyResult) -> None: + """Plot the median temporal intensity and pointwise 68 %/95 % resampling bands. + + Requires the bootstrap to have been run with ``collect_profiles=True``. The profiles + are peak-aligned (translation gauge removed by ``shiftnorm``), so the band is the + spread of the aligned intensity envelope across replicates. The bands are + pointwise, not simultaneous bands for the whole curve. + """ + if uresult.profiles is None or uresult.t_profile is None: + raise ValueError( + "uresult has no profiles; run the bootstrap with collect_profiles=True" + ) + t = np.asarray(uresult.t_profile, dtype=float) / 1e-15 + profiles = np.asarray(uresult.profiles, dtype=float) + med = np.median(profiles, axis=0) + lo68, hi68 = np.percentile(profiles, [16.0, 84.0], axis=0) + lo95, hi95 = np.percentile(profiles, [2.5, 97.5], axis=0) + ax.fill_between(t, lo95, hi95, color="C0", alpha=0.15, label="95 %") + ax.fill_between(t, lo68, hi68, color="C0", alpha=0.30, label="68 %") + ax.plot(t, med, c="C0", lw=1.5, label="median") + ax.set_xlabel("Time (fs)") + ax.set_ylabel("Intensity (a.u.)") + ax.set_ylim(bottom=0.0) + ax.set_title("Temporal intensity") + # zoom to where the envelope carries signal + support = t[med > 0.01 * med.max()] + if support.size: + pad = 0.2 * (support.max() - support.min() + 1.0) + ax.set_xlim(support.min() - pad, support.max() + pad) + ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") + + +def plot_uncertainty( + uresult: UncertaintyResult, + *, + truth: float | None = None, + figsize=(11.0, 4.2), + fig: Figure | None = None, +) -> Figure: + """Composite uncertainty figure: FWHM histogram + a confidence band if present. + + Parameters + ---------- + uresult : UncertaintyResult + The bootstrap result. + truth : float, optional + Known true FWHM (s) for the histogram (synthetic tests). + figsize : tuple, optional + Figure size. + fig : matplotlib.figure.Figure, optional + Existing figure to draw into (cleared first), e.g. a Qt canvas figure. + + Returns + ------- + matplotlib.figure.Figure + """ + # A thickness-systematic result carries per-draw thicknesses; show the dedicated + # sensitivity figure so the GUI canvas path needs no special-casing. + if getattr(uresult, "thicknesses", None) is not None: + return plot_thickness_sensitivity(uresult, truth=truth, fig=fig) + fig = _prepare_fig(fig, figsize) + if uresult.profiles is not None and uresult.t_profile is not None: + ax_h, ax_b = fig.subplots(1, 2) + plot_fwhm_distribution(ax_h, uresult, truth=truth) + plot_intensity_band(ax_b, uresult) + else: + ax_h = fig.subplots(1, 1) + plot_fwhm_distribution(ax_h, uresult, truth=truth) + fig.suptitle(uresult.summary(), fontsize="small") + return fig + + +# --------------------------------------------------------------------------- +# Substrate-thickness systematic +# --------------------------------------------------------------------------- +def _mark_thickness_prior(ax, uresult: UncertaintyResult) -> None: + """Shade the measured thickness prior ``L₀ ± σ_L`` (µm) on a thickness axis.""" + if uresult.thickness_central is None or uresult.thickness_sigma is None: + return + central_um = uresult.thickness_central / 1e-6 + sigma_um = uresult.thickness_sigma / 1e-6 + ax.axvspan( + central_um - sigma_um, + central_um + sigma_um, + color="0.8", + alpha=0.5, + label="L₀ ± σ_L", + ) + ax.axvline(central_um, color="0.4", ls=":", lw=1.0) + + +def plot_fwhm_vs_thickness( + ax, uresult: UncertaintyResult, *, truth: float | None = None +) -> None: + """Scatter the retrieved FWHM against the drawn substrate thickness. + + The horizontal bands give the point estimate and its 68 % interval; the vertical + band is the measured thickness prior. The slope of the cloud is the + sensitivity ``dFWHM/dL`` the analysis propagates; solver variation adds to + the spread. + + Parameters + ---------- + ax : matplotlib.axes.Axes + Axis to draw into. + uresult : UncertaintyResult + A ``method="thickness"`` result (its ``thicknesses`` are the x-values). + truth : float, optional + A known true FWHM (s) to mark with a dashed line (synthetic tests). + """ + if uresult.thicknesses is None: + raise ValueError("uresult has no thicknesses; run method='thickness'") + thick_um = np.asarray(uresult.thicknesses, dtype=float) / 1e-6 + fwhm_fs = np.asarray(uresult.samples, dtype=float) / 1e-15 + _mark_thickness_prior(ax, uresult) + ax.scatter(thick_um, fwhm_fs, s=14, color="C0", alpha=0.6, edgecolors="none") + ax.axhline(uresult.point_estimate / 1e-15, color="C3", lw=1.5, label="estimate") + lo68, hi68 = uresult.interval_68 + ax.axhspan(lo68 / 1e-15, hi68 / 1e-15, color="C3", alpha=0.15, label="68 %") + if truth is not None: + ax.axhline(truth / 1e-15, color="0.35", ls="--", lw=1.2, label="truth") + ax.set_xlabel("Substrate thickness (µm)") + ax.set_ylabel("FWHM (fs)") + ax.set_title("FWHM vs thickness") + ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") + + +def plot_error_vs_thickness(ax, uresult: UncertaintyResult) -> None: + """Scatter the FROG error against the drawn thickness (a consistency check). + + A flat cloud means the data do not constrain the thickness (the full prior + propagates); a clear minimum near ``L₀`` means the trace itself prefers a + thickness under the chosen model. The plot does not reweight the prior. + """ + if uresult.thicknesses is None: + raise ValueError("uresult has no thicknesses; run method='thickness'") + thick_um = np.asarray(uresult.thicknesses, dtype=float) / 1e-6 + errors = np.asarray(uresult.frog_errors, dtype=float) + _mark_thickness_prior(ax, uresult) + ax.scatter(thick_um, errors, s=14, color="C1", alpha=0.6, edgecolors="none") + ax.set_xlabel("Substrate thickness (µm)") + ax.set_ylabel("FROG error R") + ax.set_title("Trace misfit vs thickness") + ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") + + +def plot_thickness_sensitivity( + uresult: UncertaintyResult, + *, + truth: float | None = None, + figsize=(13.0, 4.2), + fig: Figure | None = None, +) -> Figure: + """Composite figure for the substrate-thickness systematic. + + Panels: FWHM vs thickness, FROG error vs thickness, and — when profiles were + collected — the temporal intensity confidence band. + + Parameters + ---------- + uresult : UncertaintyResult + A ``method="thickness"`` result. + truth : float, optional + Known true FWHM (s) for the FWHM panel (synthetic tests). + figsize : tuple, optional + Figure size. + fig : matplotlib.figure.Figure, optional + Existing figure to draw into (cleared first), e.g. a Qt canvas figure. + + Returns + ------- + matplotlib.figure.Figure + """ + fig = _prepare_fig(fig, figsize) + has_band = uresult.profiles is not None and uresult.t_profile is not None + axes = fig.subplots(1, 3 if has_band else 2) + plot_fwhm_vs_thickness(axes[0], uresult, truth=truth) + plot_error_vs_thickness(axes[1], uresult) + if has_band: + plot_intensity_band(axes[2], uresult) + fig.suptitle(uresult.summary(), fontsize="small") + return fig diff --git a/croak/plotting/units.py b/croak/plotting/units.py new file mode 100644 index 0000000..3de6b46 --- /dev/null +++ b/croak/plotting/units.py @@ -0,0 +1,78 @@ +"""Display units and number formatting shared by the plotters. + +Retrieval quantities are SI (s, rad/s, W); the axes show fs, PHz and an +SI-prefixed power, and read-outs use three significant digits (:func:`sig3`). +""" + +from __future__ import annotations + +import numpy as np + +_TWOPI_PHZ = 2 * np.pi * 1e15 + + +def sig3(value: float) -> str: + """Format a number to three significant digits, for a read-out. + + Three significant digits keeps a read-out equally informative across the + ranges croak covers (sub-fs attosecond pulses through hundreds of fs; + nanometre-wide through hundred-nanometre-wide spectra), where a fixed number + of decimals is too coarse at one end and spuriously precise at the other. + Unlike the ``g`` format this keeps trailing zeros (so ``7.00``, not ``7``) and + never switches to exponent notation, both of which read badly beside a sibling + entry in a legend or a table column. + + Parameters + ---------- + value : float + The number to format, already in its display unit. + + Returns + ------- + str + The formatted number, with no unit attached. + + Examples + -------- + >>> sig3(9.512), sig3(7.0), sig3(0.85), sig3(953.2) + ('9.51', '7.00', '0.850', '953') + """ + if not np.isfinite(value) or value == 0.0: + return f"{value:.2f}" + # digits before the decimal point (<= 0 for |value| < 1), so 3 - that many is + # how many decimals are still needed to reach three significant digits. + integer_digits = int(np.floor(np.log10(abs(value)))) + 1 + return f"{value:.{max(0, 3 - integer_digits)}f}" + + +def _fs(seconds: float) -> str: + """Format a duration (s) as femtoseconds to three significant digits.""" + return sig3(seconds / 1e-15) + + +_POWER_PREFIXES: tuple[tuple[float, str], ...] = ( + (1e15, "PW"), + (1e12, "TW"), + (1e9, "GW"), + (1e6, "MW"), + (1e3, "kW"), + (1.0, "W"), + (1e-3, "mW"), + (1e-6, "µW"), +) + + +def si_power(watts: float) -> tuple[float, str]: + """Return ``(scale, unit)`` to display ``watts`` with an SI power prefix. + + Picks the largest prefix not exceeding ``|watts|`` (so e.g. ``1.2e7`` W → + ``(1e6, "MW")``). Non-finite or non-positive inputs fall back to plain watts; + values below the smallest prefix use that smallest prefix. + """ + w = abs(float(watts)) + if not np.isfinite(w) or w <= 0: + return 1.0, "W" + for scale, unit in _POWER_PREFIXES: + if w >= scale: + return scale, unit + return _POWER_PREFIXES[-1] diff --git a/croak/plotting/updates.py b/croak/plotting/updates.py new file mode 100644 index 0000000..1d704ce --- /dev/null +++ b/croak/plotting/updates.py @@ -0,0 +1,240 @@ +"""In-place updates of the retrieval panels for the GUI's live preview.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from numpy.typing import ArrayLike + +from .artists import ( + ConvergenceArtists, + LineArtists, + MarginalArtists, + ResidualArtists, + SpectralArtists, + SpectrogramArtists, + TemporalArtists, + TraceArtists, +) +from .panels import RetrievalPlotData, _filter_factor, _trace_label +from .pulse import ( + _dispersion_text, + _edge_text, + _intensity_ylim, + _phase_ylim, + _spectral_curves, + _temporal_curves, + _truth_intensity, + _truth_spectral_phase, +) +from .traces import _signed_magnitude + +if TYPE_CHECKING: + from matplotlib.axes import Axes + + +# --------------------------------------------------------------------------- +# In-place updates +# --------------------------------------------------------------------------- +# Each update_(artists, data) pushes a new result onto the artists its +# draw_ twin created: set_data / set_array / set_text and explicit limits, +# never new artists. The invariant is update(draw(d1), d2) ≡ draw(d2), which the +# tests check panel by panel. Whether an update is possible at all — same grid +# shapes, same optional artists — is decided by _update_signature. + + +def _axes_of(artist) -> Axes: + """Return the axes an artist is drawn on (an updated artist always has one).""" + ax = artist.axes + if ax is None: + raise RuntimeError("the artist is not on any axes") + return ax + + +def _update_trace(artists: TraceArtists, data: RetrievalPlotData, C, *, db: bool): + mag, neg, vmin, vhi = _signed_magnitude( + C, db=db, tracedb=data.tracedb, vmax=None if db else data.lin_vmax + ) + artists.mesh_pos.set_array(np.ma.masked_where(neg, mag)) + artists.mesh_pos.set_clim(vmin, vhi) # a shared colorbar follows the norm + if artists.mesh_neg is not None: + artists.mesh_neg.set_array(np.ma.masked_where(~neg, mag)) + artists.mesh_neg.set_clim(vmin, vhi) + + +def update_measured_log(artists: TraceArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_measured_log`'s artists in place.""" + _update_trace(artists, data, data.measured, db=True) + + +def update_retrieved_log(artists: TraceArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_retrieved_log`'s artists in place.""" + _update_trace(artists, data, data.retrieved, db=True) + + +def update_measured_lin(artists: TraceArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_measured_lin`'s artists in place.""" + _update_trace(artists, data, data.measured, db=False) + + +def update_retrieved_lin(artists: TraceArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_retrieved_lin`'s artists in place.""" + _update_trace(artists, data, data.retrieved, db=False) + if artists.label is not None: + artists.label.set_text(_trace_label(data)) + + +def update_temporal(artists: TemporalArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_temporal`'s artists in place.""" + pr = data.processed + t = pr.t_over / 1e-15 + retr, tl, r_label, tl_label, ylabel, power_scale = _temporal_curves( + pr, data.power_scale + ) + artists.line_retr.set_data(t, retr) + artists.line_tl.set_data(pr.t_tl / 1e-15, tl) + artists.legend_retr.set_text(r_label) + artists.legend_tl.set_text(tl_label) + ax = _axes_of(artists.line_retr) + ax.set_ylabel(ylabel) + truth_curve = None + if artists.line_truth is not None and data.truth is not None: + truth_curve = _truth_intensity(data.truth, pr.energy, power_scale) + artists.line_truth.set_ydata(truth_curve) + ax.set_ylim(*_intensity_ylim(retr, tl, truth_curve)) + artists.line_phase.set_data(t[pr.mask_t], pr.phi_t[pr.mask_t]) + truth_phase = None # static within a run: the truth does not change + if artists.line_truth_phase is not None: + truth_phase = np.asarray(artists.line_truth_phase.get_ydata(), dtype=float) + limits = _phase_ylim(pr.phi_t[pr.mask_t], truth_phase) + if limits is not None: + artists.ax_phase.set_ylim(*limits) + + +def update_spectral(artists: SpectralArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_spectral`'s artists in place.""" + pr = data.processed + x, retrieved, measured, _xlabel = _spectral_curves(pr, data.spectral_axis) + artists.line_retr.set_data(x, retrieved) + if artists.line_meas is not None and measured is not None: + artists.line_meas.set_data(x, measured) + artists.line_phase.set_data(x[pr.mask_w], pr.phi_w[pr.mask_w]) + artists.line_phase_fit.set_data(x[pr.mask_w], pr.phi_w_fit[pr.mask_w]) + # The truth phase is static but its mask (where the spectrum is bright) moves. + tphi = _truth_spectral_phase(pr.wavelength, data.truth) + if artists.line_truth_phase is not None and tphi is not None: + artists.line_truth_phase.set_data(x[pr.mask_w], tphi[pr.mask_w]) + limits = _phase_ylim(pr.phi_w[pr.mask_w], None if tphi is None else tphi[pr.mask_w]) + if limits is not None: + artists.ax_phase.set_ylim(*limits) + artists.text_dispersion.set_text(_dispersion_text(pr)) + edge, edge_color = _edge_text(pr) + artists.text_edge.set_text(edge) + artists.text_edge.set_color(edge_color) + + +def update_convergence_curve(artists: ConvergenceArtists, errors: ArrayLike) -> None: + """Grow the error curve of :func:`plot_convergence` in place and rescale.""" + errors = np.asarray(errors, dtype=float) + artists.line.set_data(np.arange(1, errors.size + 1), errors) + ax = _axes_of(artists.line) + ax.relim() # autoscaling is still on here: plot_convergence sets no limits + ax.autoscale_view() + + +def update_convergence(artists: ConvergenceArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_convergence`'s artists in place.""" + update_convergence_curve(artists, data.processed.errors) + + +def update_residual(artists: ResidualArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_residual`'s artists in place.""" + resid = data.processed.residual + if artists.mesh is None or resid is None: + return + mr = float(np.max(np.abs(resid))) or 1.0 + artists.mesh.set_array(resid) + artists.mesh.set_clim(-mr, mr) + + +def update_spectral_filter(artists: LineArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_spectral_filter`'s artists in place (x is the fixed grid).""" + artists.line.set_ydata(_filter_factor(data.result)) + + +def update_freq_marginal(artists: MarginalArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_freq_marginal`'s artists in place.""" + pr = data.processed + artists.line_retr.set_ydata(pr.omega_marg_retr) + if artists.line_meas is not None and pr.omega_marg_meas is not None: + artists.line_meas.set_ydata(pr.omega_marg_meas) + + +def update_delay_marginal(artists: MarginalArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_delay_marginal`'s artists in place.""" + pr = data.processed + artists.line_retr.set_ydata(pr.tau_marg_retr) + if artists.line_meas is not None and pr.tau_marg_meas is not None: + artists.line_meas.set_ydata(pr.tau_marg_meas) + + +def update_spectrogram(artists: SpectrogramArtists, data: RetrievalPlotData) -> None: + """Update :func:`draw_spectrogram`'s artists in place. + + Raises + ------ + ValueError + If the spectrogram's shape differs from the drawn one (a different grid): + the panel has to be redrawn, not updated. + """ + _tc, _lam, S = data.spectrogram + array = artists.mesh.get_array() + drawn = () if array is None else tuple(np.shape(array)) + if drawn != S.shape: + raise ValueError(f"spectrogram shape changed from {drawn} to {S.shape}") + artists.mesh.set_array(S) + artists.mesh.autoscale() # what the first draw did implicitly + + +def _update_signature(data: RetrievalPlotData) -> tuple: + """Everything that decides which artists exist and the mesh geometry. + + Two bundles with equal signatures differ only in values, so a panel drawn for + one can be updated in place for the other. Components: the trace, grid and + oversampled-axis shapes; whether the traces hold negatives (the second + colormap); which optional curves exist (residual, measured spectrum and + marginals, absolute power, the truth and its phases); how many stage-handover + rules are visible; and the fixed axis bounds. + """ + pr = data.processed + result = data.result + truth = data.truth + n_rules = sum(1 for edge in result.stage_boundaries if 0 < edge < len(pr.errors)) + return ( + data.retrieved.shape, + data.measured.shape, + result.omega.size, + result.delays.size, + pr.t_over.size, + pr.t_tl.size, + pr.wavelength.size, + bool((data.measured < 0).any()), + bool((data.retrieved < 0).any()), + pr.residual is not None, + pr.Iw_meas is not None, + pr.omega_marg_meas is not None, + pr.tau_marg_meas is not None, + pr.peak_power is not None, + truth is not None, + truth is not None and truth.phi_t is not None, + truth is not None and truth.lam is not None and truth.Iw is not None, + truth is not None and truth.lam is not None and truth.phi_w is not None, + n_rules, + data.lam_min, + data.lam_max, + data.flim, + data.halfwidth, + data.tracedb, + data.spectral_axis, + ) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index d9c7061..0daef4c 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -179,7 +179,7 @@ def test_si_power_prefixes(): def test_resample_uniform_x_leaves_uniform_axis_unchanged(): x = np.linspace(200.0, 800.0, 64) C = np.random.default_rng(0).random((10, 64)) - xu, Cu = plotting._resample_uniform_x(x, C) + xu, Cu = plotting.traces._resample_uniform_x(x, C) np.testing.assert_allclose(xu, x) np.testing.assert_allclose(Cu, C) @@ -199,7 +199,7 @@ def test_resample_uniform_x_places_feature_at_true_wavelength(): row = np.exp(-0.5 * ((lam_nm - peak_nm) / 5.0) ** 2) # narrow feature at 260 nm C = np.tile(row, (8, 1)) # (ndelay, nlam) - xu, Cu = plotting._resample_uniform_x(lam_nm, C) + xu, Cu = plotting.traces._resample_uniform_x(lam_nm, C) # on the uniform display axis the peak column maps linearly to its wavelength j = int(np.argmax(Cu.sum(axis=0))) wl = xu[0] + j / (xu.size - 1) * (xu[-1] - xu[0]) @@ -307,7 +307,7 @@ def test_sig3_formats_three_significant_digits(): "1234", ] # _fs is the femtosecond wrapper the legends use; it must not drift from sig3 - assert plotting._fs(9.512e-15) == plotting.sig3(9.512) + assert plotting.units._fs(9.512e-15) == plotting.sig3(9.512) assert plotting.sig3(float("nan")) == "nan" @@ -317,13 +317,13 @@ def test_plot_retrieval_reuses_a_precomputed_processed_result(result_and_trace): pr = processing.process_result(res, measured=trace) calls = [] - real = plotting.process_result + real = plotting.panels.process_result def counting(*args, **kwargs): calls.append(1) return real(*args, **kwargs) - plotting.process_result = counting + plotting.panels.process_result = counting try: fig = plotting.plot_retrieval( res, measured=trace, lam_min=700e-9, lam_max=950e-9, processed=pr @@ -338,7 +338,7 @@ def counting(*args, **kwargs): assert len(calls) == 1 fig.clf() finally: - plotting.process_result = real + plotting.panels.process_result = real def test_plot_retrieval_rejects_a_processed_result_from_another_retrieval( @@ -676,13 +676,13 @@ def test_spectrogram_is_lazy_and_cached(result_and_trace, monkeypatch): """The Gabor transform runs only for a page that shows it, and only once.""" res, trace = result_and_trace calls = [] - real = plotting.spectrogram + real = plotting.panels.spectrogram def counted(*args, **kwargs): calls.append(1) return real(*args, **kwargs) - monkeypatch.setattr(plotting, "spectrogram", counted) + monkeypatch.setattr(plotting.panels, "spectrogram", counted) data = _plot_data(res, trace) assert calls == [] plotting.plot_retrieval_page(data, plotting.RETRIEVAL_PAGES["traces"]) @@ -856,8 +856,8 @@ def test_update_temporal_legend_follows_the_fwhm(update_results): artists = plotting.draw_temporal(Figure().add_subplot(), _plot_data(r1, trace)) plotting.update_temporal(artists, _plot_data(r2, trace)) pr2 = processing.process_result(r2, measured=trace) - assert plotting._fs(pr2.fwhm_retr) in artists.legend_retr.get_text() - assert plotting._fs(pr2.fwhm_tl) in artists.legend_tl.get_text() + assert plotting.units._fs(pr2.fwhm_retr) in artists.legend_retr.get_text() + assert plotting.units._fs(pr2.fwhm_tl) in artists.legend_tl.get_text() def test_update_convergence_curve_grows_and_rescales(): @@ -899,11 +899,15 @@ def test_update_signature_separates_shapes_from_values(update_results): d1 = _plot_data(r1, trace) d2 = _plot_data(r2, trace) d3 = _plot_data(r3, trace3) - assert plotting._update_signature(d1) == plotting._update_signature(d2) - assert plotting._update_signature(d1) != plotting._update_signature(d3) + assert plotting.updates._update_signature(d1) == plotting.updates._update_signature( + d2 + ) + assert plotting.updates._update_signature(d1) != plotting.updates._update_signature( + d3 + ) g = Grid(96, dt=0.5e-15) truth = TruthPulse.from_spectrum(g, gaussian_pulse(g, 7e-15), float(wlfreq(800e-9))) - assert plotting._update_signature(d1) != plotting._update_signature( + assert plotting.updates._update_signature(d1) != plotting.updates._update_signature( _plot_data(r1, trace, truth=truth) ) @@ -1019,9 +1023,9 @@ def test_retrieval_plot_data_carries_the_spectral_axis(result_and_trace): plotting.draw_spectral(ax, by_wavelength) assert ax.get_xlabel() == "Wavelength (nm)" # a different abscissa is a different set of artists: no in-place update - assert plotting._update_signature(default) != plotting._update_signature( - by_wavelength - ) + assert plotting.updates._update_signature( + default + ) != plotting.updates._update_signature(by_wavelength) fig = plotting.plot_retrieval(res, measured=trace, spectral_axis="wavelength") spectrum = next(a for a in fig.axes if a.get_title() == "Spectrum") assert spectrum.get_xlabel() == "Wavelength (nm)" @@ -1229,11 +1233,11 @@ def test_cmap_negwhite_without_a_negative_part_is_cmap_white(): def test_si_power_below_the_smallest_prefix_uses_it(): scale, unit = plotting.si_power(1e-30) - assert (scale, unit) == plotting._POWER_PREFIXES[-1] + assert (scale, unit) == plotting.units._POWER_PREFIXES[-1] def test_phase_ylim_of_nothing_finite_is_none(): - assert plotting._phase_ylim(None, np.array([np.nan]), np.array([])) is None + assert plotting.pulse._phase_ylim(None, np.array([np.nan]), np.array([])) is None def test_plot_residual_and_spectrogram_defaults(result_and_trace): @@ -1296,7 +1300,7 @@ def test_axes_of_an_unplaced_artist_raises(): from matplotlib.lines import Line2D with pytest.raises(RuntimeError, match="not on any axes"): - plotting._axes_of(Line2D([0], [0])) + plotting.updates._axes_of(Line2D([0], [0])) def test_trace_update_with_negative_samples_and_no_label(result_and_trace): @@ -1343,7 +1347,9 @@ def test_update_spectrogram_refuses_a_new_shape(update_results): def test_mappables_of_a_curve_panel_are_empty(): from matplotlib.lines import Line2D - assert plotting._mappables(plotting.LineArtists(line=Line2D([0], [0]))) == ( + assert plotting.retrieval._mappables( + plotting.LineArtists(line=Line2D([0], [0])) + ) == ( None, None, ) @@ -1405,14 +1411,16 @@ class _Stub: def test_decimate_for_display_strides_large_images(): C = np.arange(2500.0 * 3).reshape(2500, 3) - out, x, y = plotting._decimate_for_display(C, np.arange(3.0), np.arange(2500.0)) + out, x, y = plotting.traces._decimate_for_display( + C, np.arange(3.0), np.arange(2500.0) + ) assert out.shape == (1250, 3) and y.size == 1250 and x.size == 3 def test_resample_uniform_x_passes_short_axes_through(): x = np.array([1.0, 3.0]) C = np.ones((2, 2)) - xu, Cu = plotting._resample_uniform_x(x, C) + xu, Cu = plotting.traces._resample_uniform_x(x, C) assert xu is not None and Cu is C From 8609f46fd723f762a1fd2cbfb4ca80feefa4661f Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 21:45:17 +0000 Subject: [PATCH 05/41] Add the workflow window, Home and the stage-page base MainWindow carries the top bar (brand, context, Data/Prepare/Retrieve/ Analyse, one primary action, an overflow menu), the settings drawer (options.toml or the replay script), the menu bar and a status bar that hides when empty. Home offers the three entries, opening a session and the recent sessions. Saving a session and checking a saved result move to the Qt-free croak.session.files; save.format_options gives the TOML text. The old wizard stays the entry point until the stages move onto the window. Co-Authored-By: Claude Opus 5.5 --- croak/gui/home.py | 181 +++++++++++++++ croak/gui/settings.py | 31 +++ croak/gui/stage.py | 123 ++++++++++ croak/gui/window.py | 445 ++++++++++++++++++++++++++++++++++++ croak/gui/wizard.py | 53 +---- croak/gui/workflow.py | 11 +- croak/save.py | 27 ++- croak/session/files.py | 159 +++++++++++++ tests/gui/test_window.py | 394 +++++++++++++++++++++++++++++++ tests/gui/test_wizard.py | 9 +- tests/gui/test_workflow.py | 13 +- tests/test_session_files.py | 119 ++++++++++ 12 files changed, 1496 insertions(+), 69 deletions(-) create mode 100644 croak/gui/home.py create mode 100644 croak/gui/stage.py create mode 100644 croak/gui/window.py create mode 100644 croak/session/files.py create mode 100644 tests/gui/test_window.py create mode 100644 tests/test_session_files.py diff --git a/croak/gui/home.py b/croak/gui/home.py new file mode 100644 index 0000000..db2599d --- /dev/null +++ b/croak/gui/home.py @@ -0,0 +1,181 @@ +"""Home: what to retrieve, a session to reopen, and the recent sessions. + +Shown when no session is open, and from the brand button in the top bar. Each +tile starts a new session from one of the three entries; the recent sessions are +the ``options.toml`` files last saved or opened (:mod:`croak.gui.settings`), +each reopened with its data and, where it matches, its saved result. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime +from pathlib import Path + +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QMouseEvent +from PySide6.QtWidgets import ( + QFrame, + QGridLayout, + QHBoxLayout, + QScrollArea, + QVBoxLayout, + QWidget, +) + +from .theme import button, label + +__all__ = ["ENTRY_TILES", "Home"] + +ENTRY_TILES = ( + ( + "experimental", + "Measured trace", + "Load a FROG or dispersion-scan measurement from HDF5, NPZ or text, " + "with its spectrum, backgrounds and calibration.", + ), + ( + "simulated", + "Simulated scan", + "Load a numerically simulated scan (a Luna scansave file), with its " + "known input pulse and instrument geometry.", + ), + ( + "synthetic", + "Synthetic trace", + "Generate a trace from a pulse you specify, with dispersion and noise, " + "and test a retrieval against the known answer.", + ), +) +"""The three ways to start a session: entry, title, description.""" + + +class _Clickable(QFrame): + """A frame that acts as a button: it lights up under the pointer and clicks.""" + + clicked = Signal() + + def __init__(self, role: str) -> None: + super().__init__() + self.setObjectName(role) + self.setAttribute(Qt.WidgetAttribute.WA_Hover) + self.setCursor(Qt.CursorShape.PointingHandCursor) + + def mouseReleaseEvent(self, event: QMouseEvent) -> None: # noqa: N802 — Qt override + if event.button() == Qt.MouseButton.LeftButton and self.rect().contains( + event.position().toPoint() + ): + self.clicked.emit() + super().mouseReleaseEvent(event) + + +def _tile(title: str, text: str) -> _Clickable: + tile = _Clickable("tile") + layout = QVBoxLayout(tile) + layout.setContentsMargins(16, 14, 16, 14) + layout.setSpacing(4) + layout.addWidget(label(title, "tileTitle")) + layout.addWidget(label(text, "tileText", wrap=True)) + return tile + + +def describe_session(path: Path) -> tuple[str, str]: + """The title and detail line Home shows for a saved session at ``path``. + + The folder names the session (every session file is ``options.toml``); the + detail is the folder's parent and the file's modification time. + """ + when = datetime.fromtimestamp(path.stat().st_mtime).strftime("%d %b %Y, %H:%M") + return path.parent.name, f"{path.parent.parent} · {when}" + + +class Home(QWidget): + """The start page. + + Attributes + ---------- + start : Signal(str) + A tile was chosen: the entry to start a session from. + open_session : Signal(str) + A session to reopen: an ``options.toml`` path, or ``""`` to ask for one. + """ + + start = Signal(str) + open_session = Signal(str) + + def __init__(self) -> None: + super().__init__() + self.setObjectName("page") + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + content = QWidget() + content.setObjectName("page") + columns = QHBoxLayout(content) + columns.setContentsMargins(40, 36, 40, 24) + columns.setSpacing(40) + + main = QVBoxLayout() + main.setSpacing(14) + main.addWidget(label("What are you retrieving?", "h1")) + grid = QGridLayout() + grid.setSpacing(12) + self.tiles: dict[str, _Clickable] = {} + for index, (entry, title, text) in enumerate(ENTRY_TILES): + tile = _tile(title, text) + tile.clicked.connect(lambda entry=entry: self.start.emit(entry)) + grid.addWidget(tile, index // 2, index % 2) + self.tiles[entry] = tile + main.addLayout(grid) + actions = QHBoxLayout() + self.open_button = button("Open a session…") + self.open_button.clicked.connect(lambda: self.open_session.emit("")) + actions.addWidget(self.open_button) + actions.addStretch(1) + main.addLayout(actions) + main.addStretch(1) + columns.addLayout(main, stretch=3) + + side = QVBoxLayout() + side.setSpacing(6) + side.addWidget(label("Recent sessions", "h3")) + self.recent = QVBoxLayout() + self.recent.setSpacing(2) + side.addLayout(self.recent) + side.addStretch(1) + columns.addLayout(side, stretch=2) + + scroll.setWidget(content) + outer = QVBoxLayout(self) + outer.setContentsMargins(0, 0, 0, 0) + outer.addWidget(scroll) + self.recent_items: list[_Clickable] = [] + self._recent_widgets: list[QWidget] = [] + self.show_recent([]) + + def show_recent(self, paths: Sequence[str]) -> None: + """List the sessions at ``paths`` that still exist, newest first.""" + for old in self._recent_widgets: + self.recent.removeWidget(old) + old.deleteLater() + self._recent_widgets: list[QWidget] = [] + self.recent_items = [] + for raw in paths: + path = Path(raw) + if not path.is_file(): + continue + title, detail = describe_session(path) + item = _Clickable("item") + item.setToolTip(str(path)) + layout = QVBoxLayout(item) + layout.setContentsMargins(10, 7, 10, 7) + layout.setSpacing(2) + layout.addWidget(label(title, "itemTitle")) + layout.addWidget(label(detail, "itemText", wrap=True)) + item.clicked.connect(lambda raw=raw: self.open_session.emit(raw)) + self.recent_items.append(item) + hint = "Sessions you save or open appear here." + shown = self.recent_items or [label(hint, "itemText", wrap=True)] + for widget in shown: + self.recent.addWidget(widget) + self._recent_widgets.append(widget) diff --git a/croak/gui/settings.py b/croak/gui/settings.py index 520812c..a3c5f90 100644 --- a/croak/gui/settings.py +++ b/croak/gui/settings.py @@ -19,6 +19,7 @@ from __future__ import annotations +import json from typing import cast from PySide6.QtCore import QByteArray, QSettings, QSize @@ -26,6 +27,7 @@ from PySide6.QtWidgets import QWidget from .branding import APP_NAME, ORGANISATION_NAME +from .workflow import remember_recent __all__ = [ "KEY_WINDOW_GEOMETRY", @@ -43,10 +45,16 @@ "set_value", "text", "set_text", + "KEY_RECENT_SESSIONS", + "recent_sessions", + "remember_session", ] #: Settings key holding ``QWidget.saveGeometry()`` of the main window. KEY_WINDOW_GEOMETRY = "window/geometry" +#: Settings key holding the recently saved or opened sessions (``options.toml`` +#: paths, newest first) as JSON text. +KEY_RECENT_SESSIONS = "home/recent_sessions" #: Settings key holding the control column's width in logical pixels. KEY_CONTROLS_WIDTH = "stage/controls_width" #: First-run window size (logical px) before clamping to the screen; large enough @@ -164,3 +172,26 @@ def set_text(key: str, choice: str) -> None: store = user_settings() store.setValue(key, str(choice)) store.sync() + + +def recent_sessions() -> list[str]: + """The recently saved or opened sessions' ``options.toml`` paths, newest first. + + Stored as JSON text: an INI list of one item reads back as a bare string, and + a hand-edited or corrupt value reads as no history rather than an error. + """ + raw = text(KEY_RECENT_SESSIONS, "[]") + try: + paths = json.loads(raw) + except json.JSONDecodeError: + return [] + if not isinstance(paths, list): + return [] + return [p for p in paths if isinstance(p, str)] + + +def remember_session(path: str) -> list[str]: + """Put ``path`` first in the recent sessions and return the new list.""" + paths = remember_recent(recent_sessions(), path) + set_text(KEY_RECENT_SESSIONS, json.dumps(paths)) + return paths diff --git a/croak/gui/stage.py b/croak/gui/stage.py new file mode 100644 index 0000000..76b1772 --- /dev/null +++ b/croak/gui/stage.py @@ -0,0 +1,123 @@ +"""The base of a stage page in the workflow window. + +A page is a :class:`~croak.gui.sheet.Sheet`: a form of folding sections on the +left, and on the right an advice box, a progress strip, the derived numbers and +the stage's plots. The window owns the navigation and the one primary action; +a page says what it can do through a few hooks and does it in :meth:`act`. +""" + +from __future__ import annotations + +from PySide6.QtCore import Signal +from PySide6.QtWidgets import QVBoxLayout, QWidget + +from .help import HelpDialog, HelpSections +from .sheet import AdviceBox, DerivedGrid, Form, ProgressStrip, Sheet +from .state import WizardState + +__all__ = ["StagePage"] + + +class StagePage(QWidget): + """One stage of a session: its form beside its plots. + + Subclasses fill :attr:`form` with sections, add their plots with + :meth:`set_plots`, and override the hooks they need. + + Attributes + ---------- + changed : Signal + Emitted when what the page can do changes (it can now go on, a run + started or ended), so the window re-reads the hooks. + message : Signal(str) + A passing notice for the window's status bar. + """ + + HELP_TITLE: str = "" + """The stage's name in its help dialog.""" + HELP_INTRO: str = "" + """One paragraph on what the stage is for.""" + HELP: HelpSections = [] + """Per-setting help, ``[(section, [(label, text), ...]), ...]``.""" + + changed = Signal() + message = Signal(str) + + def __init__(self, state: WizardState, *, find: bool = False) -> None: + super().__init__() + self.state = state + self._help_dialog: HelpDialog | None = None + self.form = Form(self.HELP, find=find) + self.advice = AdviceBox() + self.strip = ProgressStrip() + self.derived = DerivedGrid() + self.derived.hide() # shown once a page adds a number + right = QWidget() + right.setObjectName("page") + self.plot_column = QVBoxLayout(right) + self.plot_column.setContentsMargins(0, 0, 0, 0) + self.plot_column.setSpacing(0) + self.plot_column.addWidget(self.strip) + body = QWidget() + self.body = QVBoxLayout(body) + self.body.setContentsMargins(16, 12, 16, 12) + self.body.setSpacing(10) + self.body.addWidget(self.advice) + self.body.addWidget(self.derived) + self.plot_column.addWidget(body, stretch=1) + self.sheet = Sheet(self.form, right) + outer = QVBoxLayout(self) + outer.setContentsMargins(0, 0, 0, 0) + outer.addWidget(self.sheet) + + def add_derived(self, name: str, tooltip: str = "") -> None: + """Add a derived number above the plots (shown as an en dash until set).""" + self.derived.add(name, tooltip) + self.derived.show() + + def set_plots(self, plots: QWidget) -> None: + """Put the stage's plots under its derived numbers; they take the room.""" + self.body.addWidget(plots, stretch=1) + + # -- hooks the window reads --------------------------------------------- + def on_enter(self) -> None: + """Refresh as the page comes on screen (default: nothing to do).""" + + def can_advance(self) -> bool: + """Whether the primary action can be pressed now (default: yes).""" + return True + + def busy(self) -> bool: + """Whether the page is running work the primary action would stop.""" + return False + + def page_key(self) -> str: + """Which sub-page is shown, for pages that have several (default: "").""" + return "" + + def act(self, kind: str) -> None: + """Do the primary action ``kind`` (see :class:`croak.gui.workflow.Action`). + + By default ``"continue"`` has nothing to do before the window moves on. + + Raises + ------ + ValueError + If this page has no such action. + """ + if kind == "continue": + return + raise ValueError(f"{type(self).__name__} has no {kind!r} action") + + # -- help --------------------------------------------------------------- + def show_help(self) -> HelpDialog | None: + """Open (or raise) the stage's help dialog; ``None`` if it has no help.""" + if not self.HELP: + return None + if self._help_dialog is None: + self._help_dialog = HelpDialog( + self.HELP_TITLE, self.HELP_INTRO, self.HELP, self + ) + self._help_dialog.show() + self._help_dialog.raise_() + return self._help_dialog diff --git a/croak/gui/window.py b/croak/gui/window.py new file mode 100644 index 0000000..d745809 --- /dev/null +++ b/croak/gui/window.py @@ -0,0 +1,445 @@ +"""The workflow window: Home, then Data, Prepare, Retrieve and Analyse. + +The top bar carries the brand (back to Home), what the session is, the four +stage buttons, the one primary action and an overflow menu (⋯). Which stages +open and what the primary action does are the pure rules of +:mod:`croak.gui.workflow`; this module applies them to widgets. A drawer on the +right shows the session's settings as ``options.toml`` or as a Python script. + +The pages are built by a factory from the shared :class:`WizardState`, and rebuilt +for each new session, since their controls bind to the state's parameter objects. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from pathlib import Path + +from PySide6.QtCore import Qt +from PySide6.QtGui import QAction, QCloseEvent, QKeySequence +from PySide6.QtWidgets import ( + QApplication, + QFileDialog, + QFrame, + QHBoxLayout, + QMainWindow, + QMenu, + QMessageBox, + QPlainTextEdit, + QPushButton, + QStackedWidget, + QToolButton, + QVBoxLayout, + QWidget, +) + +from .. import save +from ..scripting import generate_script +from ..session import files as session_files +from . import settings +from .branding import app_icon +from .home import ENTRY_TILES, Home +from .stage import StagePage +from .state import WizardState +from .theme import button, frame, label, load_fonts, set_flag, stylesheet +from .workflow import ( + STAGES, + Action, + Progress, + Stage, + blocked_reason, + next_stage, + primary_action, +) + +__all__ = ["DRAWER_WIDTH", "MainWindow", "PageFactory"] + +PageFactory = Callable[[WizardState], Mapping[Stage, StagePage]] +"""Builds a session's four stage pages from the shared state.""" + +DRAWER_WIDTH = 440 +"""The width of the settings drawer, logical px.""" +TOP_BAR_HEIGHT = 52 +"""The height of the top bar, logical px.""" +NOTICE_MS = 20_000 +"""How long a passing notice stays in the status bar, ms.""" + +_ENTRY_TITLES = {entry: title for entry, title, _ in ENTRY_TILES} + + +class MainWindow(QMainWindow): + """The croak window. + + Parameters + ---------- + make_pages : PageFactory + Builds the Data, Prepare, Retrieve and Analyse pages for a session. + state : WizardState, optional + The shared session state (a fresh one by default). + """ + + def __init__(self, make_pages: PageFactory, state: WizardState | None = None): + super().__init__() + load_fonts() + self.setWindowTitle("croak") + self.setWindowIcon(app_icon()) + settings.restore_window(self) + self.state = state or WizardState() + self.make_pages = make_pages + self.pages: dict[Stage, StagePage] = {} + self.stage = Stage.HOME + self.session_path = "" + + central = QWidget() + central.setObjectName("page") + self.setCentralWidget(central) + column = QVBoxLayout(central) + column.setContentsMargins(0, 0, 0, 0) + column.setSpacing(0) + column.addWidget(self._make_top()) + middle = QHBoxLayout() + middle.setContentsMargins(0, 0, 0, 0) + middle.setSpacing(0) + self.stack = QStackedWidget() + self.home = Home() + self.home.start.connect(self.new_session) + self.home.open_session.connect(self.open_session) + self.stack.addWidget(self.home) + middle.addWidget(self.stack, stretch=1) + middle.addWidget(self._make_drawer()) + column.addLayout(middle, stretch=1) + self._make_menus() + status = self.statusBar() + status.hide() + status.messageChanged.connect(lambda text: status.setVisible(bool(text))) + self.setStyleSheet(stylesheet()) + for signal in ( + self.state.load_data_changed, + self.state.tracedata_changed, + self.state.result_changed, + self.state.uncertainty_changed, + ): + signal.connect(self.refresh) + self.show_home() + + # -- building ------------------------------------------------------------- + def _make_top(self) -> QFrame: + top = frame("top") + top.setFixedHeight(TOP_BAR_HEIGHT) + bar = QHBoxLayout(top) + bar.setContentsMargins(16, 0, 16, 0) + bar.setSpacing(12) + self.brand = button("croak", "brand") + self.brand.setToolTip("Home") + self.brand.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.brand.clicked.connect(self.go_home) + self.context = label("", "context") + bar.addWidget(self.brand) + bar.addWidget(self.context) + bar.addStretch(1) + self.stage_buttons: dict[Stage, QPushButton] = {} + for stage in STAGES: + b = button(stage.title, "stage") + b.setFocusPolicy(Qt.FocusPolicy.NoFocus) + b.clicked.connect(lambda _=False, stage=stage: self.go(stage)) + bar.addWidget(b) + self.stage_buttons[stage] = b + bar.addStretch(1) + self.primary = button("", "primary") + self.primary.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.primary.clicked.connect(self.do_primary) + bar.addWidget(self.primary) + self.overflow = QToolButton() + self.overflow.setObjectName("overflow") + self.overflow.setText("⋯") + self.overflow.setToolTip("More") + self.overflow.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + self.overflow.setFocusPolicy(Qt.FocusPolicy.NoFocus) + bar.addWidget(self.overflow) + return top + + def _make_drawer(self) -> QFrame: + self.drawer = frame("drawer") + self.drawer.setFixedWidth(DRAWER_WIDTH) + layout = QVBoxLayout(self.drawer) + layout.setContentsMargins(16, 12, 16, 12) + head = QHBoxLayout() + self.drawer_title = label("", "h3") + copy = button("Copy") + copy.clicked.connect(self.copy_drawer) + close = button("Close") + close.clicked.connect(self.drawer.hide) + head.addWidget(self.drawer_title, stretch=1) + head.addWidget(copy) + head.addWidget(close) + layout.addLayout(head) + self.drawer_text = QPlainTextEdit() + self.drawer_text.setObjectName("drawerText") + self.drawer_text.setReadOnly(True) + layout.addWidget(self.drawer_text, stretch=1) + self.drawer.hide() + return self.drawer + + def _action( + self, + text: str, + slot: Callable[[], object], + shortcut: QKeySequence | None = None, + ) -> QAction: + action = QAction(text, self) + if shortcut is not None: + action.setShortcut(shortcut) + action.triggered.connect(lambda _=False: slot()) + return action + + def _make_menus(self) -> None: + keys = QKeySequence.StandardKey + self.actions_: dict[str, QAction] = { + "home": self._action("Home", self.go_home), + "open": self._action( + "Open a session…", + lambda: self.open_session(""), + QKeySequence(keys.Open), + ), + "save": self._action( + "Save session…", self.save_session, QKeySequence(keys.Save) + ), + "options": self._action( + "Show the settings (options.toml)", self.show_options + ), + "python": self._action("Show as a Python script", self.show_python), + "help": self._action( + "Help for this stage", self.show_help, QKeySequence(keys.HelpContents) + ), + } + overflow = QMenu(self) + for key in ("save", "open"): + overflow.addAction(self.actions_[key]) + overflow.addSeparator() + for key in ("options", "python"): + overflow.addAction(self.actions_[key]) + overflow.addSeparator() + overflow.addAction(self.actions_["help"]) + self.overflow.setMenu(overflow) + bar = self.menuBar() + file_menu = bar.addMenu("&File") + for key in ("home", "open", "save"): + file_menu.addAction(self.actions_[key]) + view_menu = bar.addMenu("&View") + for key in ("options", "python"): + view_menu.addAction(self.actions_[key]) + bar.addMenu("&Help").addAction(self.actions_["help"]) + + def _build_pages(self) -> None: + """Replace the stage pages with fresh ones bound to the state's parameters.""" + for page in self.pages.values(): + self.stack.removeWidget(page) + page.deleteLater() + self.pages = dict(self.make_pages(self.state)) + missing = set(STAGES) - set(self.pages) + if missing: + names = ", ".join(sorted(s.value for s in missing)) + raise ValueError(f"the page factory built no page for {names}") + for page in self.pages.values(): + self.stack.addWidget(page) + page.changed.connect(self.refresh) + page.message.connect(self.notify) + + # -- the route ------------------------------------------------------------ + def progress(self) -> Progress: + """What the session holds, for the route rules.""" + if not self.pages: + return Progress() + state = self.state + data = self.pages[Stage.DATA] + return Progress( + entry=state.entry, + data_ready=data.can_advance(), + has_load_data=state.load_data is not None, + has_tracedata=state.tracedata is not None, + has_result=state.result is not None, + raw_direct=state.entry == "simulated" and state.simulated.raw_direct, + retrieving=self.pages[Stage.RETRIEVE].busy(), + estimating=self.pages[Stage.ANALYSE].busy(), + ) + + def current_action(self) -> Action: + """The primary action the top bar offers now.""" + page = self.pages.get(self.stage) + key = page.page_key() if page is not None else "" + return primary_action(self.stage, self.progress(), key) + + def refresh(self) -> None: + """Re-apply the route rules to the top bar.""" + progress = self.progress() + in_session = self.stage is not Stage.HOME or bool(self.pages) + for stage, b in self.stage_buttons.items(): + reason = blocked_reason(stage, progress) if self.pages else "no session" + b.setVisible(in_session) + b.setEnabled(not reason) + b.setToolTip(reason) + set_flag(b, "current", stage is self.stage) + action = self.current_action() + self.primary.setVisible(bool(action.kind)) + self.primary.setText(action.text) + self.primary.setEnabled(action.enabled) + entry = _ENTRY_TITLES.get(progress.entry, "") + name = Path(self.session_path).parent.name if self.session_path else "" + self.context.setText(" · ".join(p for p in (entry, name) if p)) + self.actions_["save"].setEnabled(progress.has_result) + for key in ("options", "python"): + self.actions_[key].setEnabled(bool(progress.entry)) + + def go(self, stage: Stage) -> bool: + """Show ``stage`` if it can be opened; say why not otherwise.""" + if stage is Stage.HOME or not self.pages: + reason = "" if stage is Stage.HOME else "no session" + else: + reason = blocked_reason(stage, self.progress()) + if reason: + self.notify(reason) + return False + self.stage = stage + if stage is Stage.HOME: + self.home.show_recent(settings.recent_sessions()) + self.stack.setCurrentWidget(self.home) + else: + page = self.pages[stage] + self.stack.setCurrentWidget(page) + page.on_enter() + self.refresh() + return True + + def show_home(self) -> None: + """Show Home, keeping any open session.""" + self.go(Stage.HOME) + + def go_home(self) -> None: + """Show Home (the session stays open until a new one starts).""" + self.show_home() + + def do_primary(self) -> None: + """Do what the primary action says.""" + action = self.current_action() + if not action.enabled: + return + if action.kind == "save": + self.save_session() + return + page = self.pages[self.stage] + page.act(action.kind) + if action.kind == "continue": + self.go(next_stage(self.stage, self.progress())) + self.refresh() + + # -- sessions --------------------------------------------------------------- + def confirm_discard(self) -> bool: + """Ask before a new session replaces one holding data; True to go ahead.""" + if not self.state.has_data: + return True + box = QMessageBox(self) + box.setIcon(QMessageBox.Icon.Warning) + box.setWindowTitle("Start a new session?") + box.setText("The current session's trace and retrieval will be discarded.") + box.setInformativeText("Save session… in the ⋯ menu keeps them.") + discard = box.addButton("Discard", QMessageBox.ButtonRole.DestructiveRole) + box.addButton(QMessageBox.StandardButton.Cancel) + box.exec() + return box.clickedButton() is discard + + def new_session(self, entry: str) -> bool: + """Start a session from ``entry`` and open its Data stage.""" + if not self.confirm_discard(): + return False + self.state.reset() + self.state.entry = entry + self.session_path = "" + self._build_pages() + self.go(Stage.DATA) + return True + + def open_session(self, path: str) -> bool: + """Reopen the session saved at ``path`` (``""`` asks for the file).""" + if not path: + path, _ = QFileDialog.getOpenFileName( + self, "Open a session", filter="Session settings (options.toml *.toml)" + ) + if not path: + return False + if not self.confirm_discard(): + return False + try: + options = save.load_options(path) + except (OSError, ValueError) as exc: + self.notify(f"Could not open {path}: {exc}") + return False + self.state.reset() + self.state.from_options(options) + self.session_path = path + settings.remember_session(path) + self._build_pages() + # TODO(gui-redesign): replay the data, preparation and retrieval (or + # rehydrate result.h5) once the new stage pages expose them. + self.go(Stage.DATA) + return True + + def save_session(self) -> list[str]: + """Ask for a folder and save the session there; return the files written.""" + if self.state.result is None: + self.notify("Run a retrieval first: there is nothing to save yet.") + return [] + folder = QFileDialog.getExistingDirectory(self, "Save the session in") + if not folder: + return [] + written = session_files.save_session( + folder, + self.state.session_options(), + result=self.state.result, + processed=self.state.processed, + truth=self.state.truth, + uncertainty=self.state.uncertainty_results, + ) + self.session_path = str(Path(folder) / session_files.OPTIONS_FILE) + settings.remember_session(self.session_path) + self.notify(f"Saved {', '.join(written)} in {folder}") + self.refresh() + return written + + # -- the drawer and help ------------------------------------------------------ + def show_drawer(self, title: str, text: str) -> None: + """Open the drawer on ``text``.""" + self.drawer_title.setText(title) + self.drawer_text.setPlainText(text) + self.drawer.show() + + def show_options(self) -> None: + """Show the session's settings as the ``options.toml`` it would save.""" + self.show_drawer( + "Settings (options.toml)", save.format_options(self.state.to_options()) + ) + + def show_python(self) -> None: + """Show a Python script that replays the session headless.""" + self.show_drawer( + "As a Python script", generate_script(self.state.session_options()) + ) + + def copy_drawer(self) -> None: + """Put the drawer's text on the clipboard.""" + QApplication.clipboard().setText(self.drawer_text.toPlainText()) + self.notify("Copied.") + + def show_help(self) -> None: + """Open the help for the stage on screen.""" + page = self.pages.get(self.stage) + if page is None or page.show_help() is None: + self.notify("Choose a stage to see its help.") + + def notify(self, text: str) -> None: + """Show a passing notice in the status bar.""" + self.statusBar().showMessage(text, NOTICE_MS) + + def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 — Qt override + """Remember the window's geometry, then close.""" + settings.save_window(self) + super().closeEvent(event) diff --git a/croak/gui/wizard.py b/croak/gui/wizard.py index 3aa0ab6..fdfce09 100644 --- a/croak/gui/wizard.py +++ b/croak/gui/wizard.py @@ -13,7 +13,6 @@ import os -import numpy as np from PySide6.QtGui import QCloseEvent from PySide6.QtWidgets import ( QFileDialog, @@ -29,6 +28,7 @@ from .. import save from ..session import retarget +from ..session.files import result_mismatch from . import settings from .base import Stage from .branding import app_icon @@ -67,52 +67,6 @@ _STAGE_OFFSET = 3 -def _result_mismatch(saved, td, retrieve_params) -> str: - """Why a saved ``result.h5`` cannot be paired with ``td``, or ``""`` if it can. - - Reuse is only sound when the saved retrieval was fitted to *this* trace, so - the saved measured trace and frequency axis are compared with the current - ones element-wise rather than by shape. Both are stored exactly as the - retrieval saw them (``trace_meas`` is the array handed to - :func:`croak.processing.process_result`), so equality is the honest test and - any difference — a moved delay grid, a changed filter, a new calibration — - is grounds to re-run rather than to display a fit against data it never saw. - - A file too old to carry ``trace_meas`` cannot be verified at all, so it is - also refused: the fast path is an optimisation, and declining it costs only - a re-run, while wrongly taking it costs a wrong residual panel. - """ - cur = td.trace - meas = saved.get("trace_meas") - if meas is None: - return "the saved result does not store the trace it was fitted to" - meas = np.asarray(meas, float) - if meas.shape != np.shape(cur): - return ( - f"the saved result is on a {meas.shape[0]}x{meas.shape[1]} grid but " - f"this trace regrids to {np.shape(cur)[0]}x{np.shape(cur)[1]}" - ) - peak = float(np.max(np.abs(cur))) or 1.0 - if float(np.max(np.abs(meas - cur))) > 1e-9 * peak: - return "preprocessing no longer reproduces the trace the result was fitted to" - w_saved = saved.get("omega") - if w_saved is not None: - w_cur = np.asarray(td.grid.omega, float) - w_saved = np.asarray(w_saved, float) - span = float(np.max(np.abs(w_cur))) or 1.0 - if w_saved.shape != w_cur.shape or ( - float(np.max(np.abs(w_saved - w_cur))) > 1e-9 * span - ): - return "the saved result is on a different frequency grid" - algo = saved.get("algorithm") - if algo is not None and str(algo) != str(retrieve_params.solver): - return ( - f"the saved result came from {algo!s} but the loaded settings select " - f"{retrieve_params.solver}" - ) - return "" - - class Wizard(QMainWindow): """Welcome menu + synthetic generator + four-stage pulse-retrieval wizard.""" @@ -573,7 +527,8 @@ def _reload_saved_result(self, result_path: str) -> bool: A shape test alone would not establish this: any preprocessing change that alters trace values without changing its size (a filtering default, the edge taper, a recalibration) would slip through and be displayed as - a consistent fit when it is not. See :func:`_result_mismatch`. + a consistent fit when it is not. See + :func:`croak.session.files.result_mismatch`. """ td = self.state.tracedata if td is None: @@ -583,7 +538,7 @@ def _reload_saved_result(self, result_path: str) -> bool: result = save.result_from_saved(saved, grid=td.grid) except KeyError, ValueError, OSError: return False - self._stale_result_note = _result_mismatch(saved, td, self.state.retrieve) + self._stale_result_note = result_mismatch(saved, td, self.state.retrieve) if self._stale_result_note: return False # processed is recomputed by the Retrieve stage's on_enter/_update_view. diff --git a/croak/gui/workflow.py b/croak/gui/workflow.py index ec18d2e..e27201c 100644 --- a/croak/gui/workflow.py +++ b/croak/gui/workflow.py @@ -110,16 +110,21 @@ def blocked_reason(stage: Stage, progress: Progress) -> str: return "" if progress.has_result else "Run a retrieval first." -def next_stage(stage: Stage, progress: Progress) -> Stage | None: - """The stage the primary action leads to from ``stage``, if it leads on. +def next_stage(stage: Stage, progress: Progress) -> Stage: + """The stage "Continue" leads to from ``stage``. From Data a raw simulated scan goes straight to Retrieve. + + Raises + ------ + ValueError + For a stage with no "Continue" (Home, Retrieve, Analyse). """ if stage is Stage.DATA: return Stage.RETRIEVE if progress.raw_direct else Stage.PREPARE if stage is Stage.PREPARE: return Stage.RETRIEVE - return None + raise ValueError(f"{stage.title} has no next stage") @dataclass(frozen=True) diff --git a/croak/save.py b/croak/save.py index 2e75926..b707faa 100644 --- a/croak/save.py +++ b/croak/save.py @@ -33,6 +33,7 @@ "result_from_saved", "save_uncertainty", "save_options", + "format_options", "load_options", ] @@ -390,18 +391,30 @@ def _croak_version() -> str: return "unknown" -def save_options(options: dict[str, Any], path: str) -> str: - """Write GUI session options to a TOML file. +def format_options(options: dict[str, Any]) -> str: + """Return session options as the TOML text :func:`save_options` writes. + + A ``croak_version`` / ``options_version`` header comes first so older files + can be reloaded (and migrated, if ever needed) gracefully. Nested dicts (one + per stage) become TOML tables; numpy values are coerced and ``None`` values + omitted. - A ``croak_version`` / ``options_version`` header is written first so older - files can be reloaded (and migrated, if ever needed) gracefully. Nested - dicts (one per stage) become TOML tables; numpy values are coerced. + Examples + -------- + >>> text = format_options({"load": {"energy_j": 1e-6, "frog_path": None}}) + >>> "[load]" in text and "frog_path" not in text + True """ # scalar header keys must precede the table sections in TOML data = {"croak_version": _croak_version(), "options_version": OPTIONS_VERSION} data.update(_tomlify(options)) - with open(path, "wb") as f: - tomli_w.dump(data, f) + return tomli_w.dumps(data) + + +def save_options(options: dict[str, Any], path: str) -> str: + """Write GUI session options to a TOML file (see :func:`format_options`).""" + with open(path, "w", encoding="utf-8") as f: + f.write(format_options(options)) return path diff --git a/croak/session/files.py b/croak/session/files.py new file mode 100644 index 0000000..4bdfa97 --- /dev/null +++ b/croak/session/files.py @@ -0,0 +1,159 @@ +"""A session's files on disk: writing a saved session, and checking a saved result. + +A saved session is a folder holding ``options.toml`` (the settings, enough to +replay it), ``result.h5`` (the retrieval, with any uncertainty estimates +appended) and ``retrieval.pdf`` (the twelve-panel overview). Reopening one +replays the settings and reuses ``result.h5`` only when it was fitted to the +trace the replay produces (:func:`result_mismatch`). +""" + +from __future__ import annotations + +from collections.abc import Mapping +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .. import save +from .options import SessionOptions + +if TYPE_CHECKING: + from ..plotting import RetrievalPlotData + from ..preprocess import TraceData + from ..processing import ProcessedResult, TruthPulse + from ..result import RetrievalResult + from .params import RetrieveParams + +__all__ = [ + "OPTIONS_FILE", + "OVERVIEW_DPI", + "OVERVIEW_FILE", + "RESULT_FILE", + "result_mismatch", + "save_session", +] + +OPTIONS_FILE = "options.toml" +"""The settings of a saved session.""" +RESULT_FILE = "result.h5" +"""The retrieval of a saved session.""" +OVERVIEW_FILE = "retrieval.pdf" +"""The overview figure of a saved session.""" +OVERVIEW_DPI = 600 +"""The overview's resolution: its trace maps are rasterised inside the PDF.""" + +# Relative tolerance for "the same array": both sides are stored exactly as the +# retrieval saw them, so only floating-point round-off may differ. +_SAME = 1e-9 + + +def save_session( + folder: str | Path, + options: SessionOptions, + *, + result: RetrievalResult | None = None, + processed: ProcessedResult | None = None, + truth: TruthPulse | None = None, + uncertainty: Mapping[str, Any] | None = None, + overview: RetrievalPlotData | None = None, +) -> list[str]: + """Write a session into ``folder`` and return the names of the files written. + + Parameters + ---------- + folder : str or Path + An existing folder; files of the same names are overwritten. + options : SessionOptions + The settings, always written to :data:`OPTIONS_FILE`. + result, processed, truth + The retrieval, its processed form and any known truth, written to + :data:`RESULT_FILE` when ``result`` is given. + uncertainty : mapping, optional + Uncertainty estimates by method, appended to :data:`RESULT_FILE`. + overview : RetrievalPlotData, optional + Drawn as the twelve-panel overview into :data:`OVERVIEW_FILE`. + + Raises + ------ + FileNotFoundError + If ``folder`` does not exist. + ValueError + If ``uncertainty`` is given without a ``result`` to append it to. + """ + folder = Path(folder) + if not folder.is_dir(): + raise FileNotFoundError(f"no such folder: {folder}") + if uncertainty and result is None: + raise ValueError("uncertainty estimates need the result they belong to") + written = [] + if result is not None: + path = folder / RESULT_FILE + save.save_result( + result, str(path), processed=processed, truth=truth, force=True + ) + written.append(RESULT_FILE) + if uncertainty: + save.save_uncertainty(dict(uncertainty), str(path), force=True) + save.save_options(options.to_dict(), str(folder / OPTIONS_FILE)) + written.append(OPTIONS_FILE) + if overview is not None: + from .. import plotting # matplotlib only when a figure is drawn + + figure = plotting.plot_retrieval_page(overview, plotting.RETRIEVAL_OVERVIEW) + figure.savefig(folder / OVERVIEW_FILE, dpi=OVERVIEW_DPI) + written.append(OVERVIEW_FILE) + return written + + +def result_mismatch( + saved: Mapping[str, Any], tracedata: TraceData, retrieve: RetrieveParams +) -> str: + """Why a saved result cannot be paired with ``tracedata``, or ``""`` if it can. + + Reuse is only sound when the saved retrieval was fitted to *this* trace, so + the saved measured trace and frequency axis are compared with the current + ones element-wise rather than by shape: any difference (a moved delay grid, + a changed filter, a new calibration) is grounds to re-run rather than to show + a fit against data it never saw. A file too old to carry ``trace_meas`` + cannot be verified, so it is refused too: declining costs only a re-run, + wrongly accepting costs a wrong residual panel. + + Parameters + ---------- + saved : mapping + A result file as read by :func:`croak.save.load_result`. + tracedata : TraceData + The trace the replayed session produced. + retrieve : RetrieveParams + The replayed retrieval settings (their solver must match the file's). + """ + current = tracedata.trace + meas = saved.get("trace_meas") + if meas is None: + return "the saved result does not store the trace it was fitted to" + meas = np.asarray(meas, float) + if meas.shape != np.shape(current): + return ( + f"the saved result is on a {meas.shape[0]}x{meas.shape[1]} grid but " + f"this trace regrids to {np.shape(current)[0]}x{np.shape(current)[1]}" + ) + peak = float(np.max(np.abs(current))) or 1.0 + if float(np.max(np.abs(meas - current))) > _SAME * peak: + return "preprocessing no longer reproduces the trace the result was fitted to" + omega_saved = saved.get("omega") + if omega_saved is not None: + omega = np.asarray(tracedata.grid.omega, float) + omega_saved = np.asarray(omega_saved, float) + span = float(np.max(np.abs(omega))) or 1.0 + if omega_saved.shape != omega.shape or ( + float(np.max(np.abs(omega_saved - omega))) > _SAME * span + ): + return "the saved result is on a different frequency grid" + algorithm = saved.get("algorithm") + if algorithm is not None and str(algorithm) != str(retrieve.solver): + return ( + f"the saved result came from {algorithm!s} but the loaded settings " + f"select {retrieve.solver}" + ) + return "" diff --git a/tests/gui/test_window.py b/tests/gui/test_window.py new file mode 100644 index 0000000..d70d4df --- /dev/null +++ b/tests/gui/test_window.py @@ -0,0 +1,394 @@ +"""The workflow window, Home and the stage-page base, driven with stub pages.""" + +from __future__ import annotations + +import os +import time +from pathlib import Path + +import pytest + +pytest.importorskip("PySide6") + +from PySide6.QtCore import QPointF, Qt +from PySide6.QtGui import QMouseEvent +from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QWidget + +from croak import save +from croak.gui import settings +from croak.gui.home import Home, describe_session +from croak.gui.stage import StagePage +from croak.gui.state import WizardState +from croak.gui.window import MainWindow +from croak.gui.workflow import STAGES, Stage +from croak.session.options import SessionOptions + + +class StubPage(StagePage): + """A stage page whose hooks the tests set.""" + + HELP_TITLE = "Stub" + HELP = [("Section", [("Row", "Help text.")])] + + def __init__(self, state, stage): + super().__init__(state) + self.stage = stage + self.ready = True + self.running = False + self.key = "" + self.acted: list[str] = [] + self.entered = 0 + + def on_enter(self): + self.entered += 1 + + def can_advance(self): + return self.ready + + def busy(self): + return self.running + + def page_key(self): + return self.key + + def act(self, kind): + self.acted.append(kind) + if self.stage is Stage.DATA and kind == "continue": + self.state.set_load_data({"trace": "loaded"}) + + +def stub_pages(state): + return {stage: StubPage(state, stage) for stage in STAGES} + + +@pytest.fixture +def window(qtbot, monkeypatch): + # A dialog would block the test; each test says what the user answers. + monkeypatch.setattr(QMessageBox, "exec", lambda self: 0) + win = MainWindow(stub_pages) + qtbot.addWidget(win) + return win + + +def _choose(monkeypatch, answer: str): + """Make the discard confirmation answer ``answer`` ("Discard" or "Cancel").""" + + def clicked(box): + for b in box.buttons(): + if answer in b.text(): + return b + return None + + monkeypatch.setattr(QMessageBox, "clickedButton", clicked) + + +# -- Home ------------------------------------------------------------------------- +def test_window_opens_on_home_with_no_session(window): + assert window.stage is Stage.HOME + assert window.stack.currentWidget() is window.home + assert not window.primary.isVisible() + assert not any(b.isVisibleTo(window) for b in window.stage_buttons.values()) + assert not window.go(Stage.DATA) # no session to open + assert window.statusBar().currentMessage() == "no session" + assert not window.actions_["save"].isEnabled() + + +def test_window_home_tile_starts_a_session_at_data(window, qtbot): + tile = window.home.tiles["synthetic"] + press = QMouseEvent( + QMouseEvent.Type.MouseButtonRelease, + QPointF(5, 5), + QPointF(5, 5), + Qt.MouseButton.LeftButton, + Qt.MouseButton.NoButton, + Qt.KeyboardModifier.NoModifier, + ) + tile.mouseReleaseEvent(press) + assert window.stage is Stage.DATA + assert window.state.entry == "synthetic" + assert window.context.text() == "Synthetic trace" + assert window.pages[Stage.DATA].entered == 1 + assert window.stage_buttons[Stage.DATA].property("current") is True + assert not window.stage_buttons[Stage.PREPARE].isEnabled() + assert "Load a trace" in window.stage_buttons[Stage.PREPARE].toolTip() + + +def test_window_home_tile_ignores_a_right_click_or_a_release_outside(qtbot): + home = Home() + qtbot.addWidget(home) + started = [] + home.start.connect(started.append) + tile = home.tiles["experimental"] + for button, point in ( + (Qt.MouseButton.RightButton, QPointF(5, 5)), + (Qt.MouseButton.LeftButton, QPointF(-50, -50)), + ): + tile.mouseReleaseEvent( + QMouseEvent( + QMouseEvent.Type.MouseButtonRelease, + point, + point, + button, + Qt.MouseButton.NoButton, + Qt.KeyboardModifier.NoModifier, + ) + ) + assert started == [] + + +def test_window_home_lists_recent_sessions_that_still_exist(qtbot, tmp_path): + saved = tmp_path / "run-7" / "options.toml" + saved.parent.mkdir() + saved.write_text("") + home = Home() + qtbot.addWidget(home) + opened = [] + home.open_session.connect(opened.append) + home.show_recent([str(tmp_path / "gone" / "options.toml"), str(saved)]) + assert len(home.recent_items) == 1 + title, detail = describe_session(saved) + assert title == "run-7" and str(tmp_path) in detail + home.recent_items[0].clicked.emit() + assert opened == [str(saved)] + home.open_button.click() + assert opened[-1] == "" + home.show_recent([]) # back to the hint + assert home.recent_items == [] + + +# -- the route ---------------------------------------------------------------------- +def test_window_primary_action_continues_through_the_stages(window): + window.new_session("experimental") + data = window.pages[Stage.DATA] + data.ready = False + window.refresh() + assert window.primary.text() == "Continue" and not window.primary.isEnabled() + window.do_primary() # disabled: does nothing + assert data.acted == [] + data.ready = True + window.refresh() + window.do_primary() + assert data.acted == ["continue"] + assert window.stage is Stage.PREPARE + # Prepare cannot continue until a trace is prepared. + assert not window.primary.isEnabled() + window.state.set_tracedata("prepared") + assert window.primary.isEnabled() + window.do_primary() + assert window.stage is Stage.RETRIEVE + assert window.primary.text() == "Retrieve" + + +def test_window_raw_simulated_scan_continues_straight_to_retrieve(window): + window.new_session("simulated") + window.state.simulated.raw_direct = True + data = window.pages[Stage.DATA] + + def raw_continue(kind): + window.state.set_load_data({"trace": "raw"}) + window.state.set_tracedata("native grid") + + data.act = raw_continue + window.do_primary() + assert window.stage is Stage.RETRIEVE + assert not window.stage_buttons[Stage.PREPARE].isEnabled() + + +def test_window_retrieve_and_analyse_actions_reach_their_pages(window): + window.new_session("synthetic") + window.state.set_load_data({"trace": 1}) + window.state.set_tracedata("td") + assert window.go(Stage.RETRIEVE) + retrieve = window.pages[Stage.RETRIEVE] + window.do_primary() + assert retrieve.acted == ["retrieve"] + retrieve.running = True + window.refresh() + assert window.primary.text() == "Stop" + window.do_primary() + assert retrieve.acted[-1] == "stop" + retrieve.running = False + window.state.set_result("result") + assert window.primary.text() == "Retrieve again" + assert window.go(Stage.ANALYSE) + analyse = window.pages[Stage.ANALYSE] + assert window.primary.text() == "Save" + analyse.key = "uncertainty" + window.refresh() + assert window.primary.text() == "Estimate" + window.do_primary() + assert analyse.acted == ["estimate"] + # The stage buttons reach a finished stage directly. + window.stage_buttons[Stage.DATA].click() + assert window.stage is Stage.DATA + + +def test_window_brand_returns_home_keeping_the_session(window): + window.new_session("synthetic") + window.brand.click() + assert window.stage is Stage.HOME + assert window.pages # still open: the stages stay reachable + assert window.stage_buttons[Stage.DATA].isVisibleTo(window) + assert window.go(Stage.DATA) + + +def test_window_new_session_asks_before_discarding_data(window, monkeypatch): + window.new_session("experimental") + window.state.set_load_data({"trace": 1}) + _choose(monkeypatch, "Cancel") + assert not window.new_session("synthetic") + assert window.state.entry == "experimental" + _choose(monkeypatch, "Discard") + assert window.new_session("synthetic") + assert window.state.load_data is None + + +def test_window_rejects_a_factory_missing_a_stage(qtbot): + def partial(state): + return {Stage.DATA: StubPage(state, Stage.DATA)} + + win = MainWindow(partial) + qtbot.addWidget(win) + with pytest.raises(ValueError, match="no page for analyse, prepare, retrieve"): + win.new_session("synthetic") + + +def test_stage_page_base_hooks_have_defaults(qtbot): + page = StagePage(WizardState()) + qtbot.addWidget(page) + page.on_enter() + assert page.can_advance() and not page.busy() and page.page_key() == "" + page.act("continue") # nothing to do before moving on + with pytest.raises(ValueError, match="no 'retrieve' action"): + page.act("retrieve") + assert page.show_help() is None # no help written + assert page.derived.isHidden() + page.add_derived("FWHM") + assert not page.derived.isHidden() + plots = QWidget() + page.set_plots(plots) + assert plots.parent() is not None + + +# -- sessions, the drawer and help ------------------------------------------ +def test_window_opens_a_saved_session_and_remembers_it(window, tmp_path, monkeypatch): + options = SessionOptions(entry="simulated") + path = tmp_path / "run" / "options.toml" + path.parent.mkdir() + save.save_options(options.to_dict(), str(path)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(path), "")) + assert window.open_session("") + assert window.state.entry == "simulated" + assert window.stage is Stage.DATA + assert window.context.text() == "Simulated scan · run" + assert settings.recent_sessions() == [str(path)] + window.go_home() + assert len(window.home.recent_items) == 1 + + +def test_window_open_session_cancelled_or_unreadable(window, tmp_path, monkeypatch): + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: ("", "")) + assert not window.open_session("") + bad = tmp_path / "options.toml" + bad.write_text("not = [toml") + assert not window.open_session(str(bad)) + assert "Could not open" in window.statusBar().currentMessage() + window.new_session("experimental") + window.state.set_load_data({"trace": 1}) + _choose(monkeypatch, "Cancel") + assert not window.open_session(str(bad)) + + +def test_window_saves_a_session_with_a_result(window, tmp_path, monkeypatch): + window.new_session("synthetic") + assert window.save_session() == [] # nothing retrieved yet + assert "nothing to save" in window.statusBar().currentMessage() + window.state.result = "result" + window.refresh() + assert window.actions_["save"].isEnabled() + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "") + assert window.save_session() == [] # cancelled + calls = {} + + def fake_save(folder, options, **kwargs): + calls.update(folder=folder, **kwargs) + return ["result.h5", "options.toml"] + + monkeypatch.setattr("croak.session.files.save_session", fake_save) + monkeypatch.setattr( + QFileDialog, "getExistingDirectory", lambda *a, **k: str(tmp_path) + ) + assert window.save_session() == ["result.h5", "options.toml"] + assert calls["folder"] == str(tmp_path) and calls["result"] == "result" + assert settings.recent_sessions() == [str(tmp_path / "options.toml")] + # Save is also the primary action on Analyse. + window.state.set_load_data({"trace": 1}) + window.state.tracedata = "td" + window.state.result = "result" + assert window.go(Stage.ANALYSE) + window.do_primary() + assert "Saved result.h5, options.toml" in window.statusBar().currentMessage() + + +def test_window_drawer_shows_the_settings_and_the_script(window, qtbot): + window.new_session("experimental") + window.actions_["options"].trigger() + assert not window.drawer.isHidden() + assert "[retrieve]" in window.drawer_text.toPlainText() + window.actions_["python"].trigger() + assert "import croak" in window.drawer_text.toPlainText() + window.copy_drawer() + assert QApplication.clipboard().text() == window.drawer_text.toPlainText() + window.drawer.hide() + + +def test_window_help_opens_for_the_stage_on_screen(window): + window.show_help() # on Home: nothing to explain + assert "Choose a stage" in window.statusBar().currentMessage() + window.new_session("experimental") + window.actions_["help"].trigger() + dialog = window.pages[Stage.DATA]._help_dialog + assert dialog is not None and dialog.isVisible() + window.show_help() # raised again, not rebuilt + assert window.pages[Stage.DATA]._help_dialog is dialog + dialog.close() + + +def test_window_status_bar_hides_when_empty(window, qtbot): + window.show() + qtbot.waitExposed(window) + window.notify("hello") + assert window.statusBar().isVisible() + window.statusBar().clearMessage() + assert not window.statusBar().isVisible() + + +def test_window_remembers_its_geometry_on_close(window, qtbot): + window.resize(900, 700) + window.close() + assert settings.user_settings().value(settings.KEY_WINDOW_GEOMETRY) is not None + + +# -- recent sessions ---------------------------------------------------------------- +def test_settings_recent_sessions_survive_and_reject_garbage(): + assert settings.recent_sessions() == [] + settings.remember_session("/a/options.toml") + settings.remember_session("/b/options.toml") + assert settings.recent_sessions() == ["/b/options.toml", "/a/options.toml"] + settings.set_text(settings.KEY_RECENT_SESSIONS, "{not json") + assert settings.recent_sessions() == [] + settings.set_text(settings.KEY_RECENT_SESSIONS, '{"a": 1}') + assert settings.recent_sessions() == [] + settings.set_text(settings.KEY_RECENT_SESSIONS, '["/x", 3]') + assert settings.recent_sessions() == ["/x"] + + +def test_home_describes_a_session_by_folder_and_time(tmp_path): + path = tmp_path / "scan-12" / "options.toml" + path.parent.mkdir() + path.write_text("") + stamp = time.mktime((2026, 3, 4, 5, 6, 0, 0, 0, -1)) + os.utime(path, (stamp, stamp)) + title, detail = describe_session(Path(path)) + assert title == "scan-12" + assert detail.endswith("04 Mar 2026, 05:06") diff --git a/tests/gui/test_wizard.py b/tests/gui/test_wizard.py index 16089c4..2e30eb6 100644 --- a/tests/gui/test_wizard.py +++ b/tests/gui/test_wizard.py @@ -19,7 +19,8 @@ from croak import save from croak.gui import stage_preprocess, wizard from croak.gui.state import WizardState -from croak.gui.wizard import Wizard, _result_mismatch +from croak.gui.wizard import Wizard +from croak.session.files import result_mismatch @pytest.fixture @@ -37,7 +38,7 @@ def _point_at_experiment(state, experiment_h5, experiment): p.interaction = experiment.interaction -# -- _result_mismatch ----------------------------------------------------------- +# -- result_mismatch ------------------------------------------------------------ def _td(trace, omega): return SimpleNamespace(trace=trace, grid=SimpleNamespace(omega=omega)) @@ -76,14 +77,14 @@ def _td(trace, omega): ) def test_wizard_result_mismatch_explains_why_a_result_is_refused(saved, reason): td = _td(np.ones((2, 3)), np.array([1.0, 2.0])) - assert _result_mismatch(saved, td, SimpleNamespace(solver="copra")) == reason + assert result_mismatch(saved, td, SimpleNamespace(solver="copra")) == reason def test_wizard_result_mismatch_handles_an_all_zero_trace(): # a zero peak/span falls back to an absolute tolerance of 1e-9 td = _td(np.zeros((1, 2)), np.zeros(2)) saved = {"trace_meas": np.zeros((1, 2)), "omega": np.zeros(2)} - assert _result_mismatch(saved, td, SimpleNamespace(solver="copra")) == "" + assert result_mismatch(saved, td, SimpleNamespace(solver="copra")) == "" # -- navigation ------------------------------------------------------------------- diff --git a/tests/gui/test_workflow.py b/tests/gui/test_workflow.py index 0599b7d..9402130 100644 --- a/tests/gui/test_workflow.py +++ b/tests/gui/test_workflow.py @@ -60,17 +60,18 @@ def test_workflow_raw_simulated_scan_skips_prepare_with_a_reason(): @pytest.mark.parametrize( ("stage", "expected"), - [ - (Stage.DATA, Stage.PREPARE), - (Stage.PREPARE, Stage.RETRIEVE), - (Stage.RETRIEVE, None), - (Stage.ANALYSE, None), - ], + [(Stage.DATA, Stage.PREPARE), (Stage.PREPARE, Stage.RETRIEVE)], ) def test_workflow_next_stage_follows_the_session(stage, expected): assert next_stage(stage, LOADED) is expected +@pytest.mark.parametrize("stage", [Stage.HOME, Stage.RETRIEVE, Stage.ANALYSE]) +def test_workflow_next_stage_refuses_a_stage_without_continue(stage): + with pytest.raises(ValueError, match="has no next stage"): + next_stage(stage, LOADED) + + @pytest.mark.parametrize( ("stage", "progress", "page", "expected"), [ diff --git a/tests/test_session_files.py b/tests/test_session_files.py new file mode 100644 index 0000000..c98d5de --- /dev/null +++ b/tests/test_session_files.py @@ -0,0 +1,119 @@ +"""A session's files: saving a session folder and checking a saved result.""" + +from __future__ import annotations + +from dataclasses import replace + +import numpy as np +import pytest +from test_session import _options_for + +from croak import save +from croak.session import run_session +from croak.session.files import ( + OPTIONS_FILE, + OVERVIEW_FILE, + RESULT_FILE, + result_mismatch, + save_session, +) + + +@pytest.fixture(scope="module") +def session(tmp_path_factory): + """A short real retrieval of the synthetic experiment, with its options.""" + from conftest import make_synthetic_experiment, write_experiment_h5 + + experiment = make_synthetic_experiment() + path = write_experiment_h5( + str(tmp_path_factory.mktemp("data") / "exp.h5"), experiment + ) + options = _options_for(path, experiment) + options = replace(options, retrieve=replace(options.retrieve, maxiters=5)) + out = run_session( + options, + stages=("load", "preproc", "retrieve", "process"), + rng=np.random.default_rng(0), + ) + return options, out + + +def test_session_files_save_options_alone(tmp_path, session): + options, _ = session + assert save_session(tmp_path, options) == [OPTIONS_FILE] + assert save.load_options(str(tmp_path / OPTIONS_FILE))["retrieve"]["maxiters"] == 5 + + +def test_session_files_save_result_uncertainty_and_overview(tmp_path, session): + from croak.plotting import retrieval_plot_data + + options, out = session + td = out.tracedata + overview = retrieval_plot_data(out.result, measured=td.trace, processed=None) + written = save_session( + tmp_path, + options, + result=out.result, + processed=out.processed, + uncertainty={}, # nothing estimated: nothing appended + overview=overview, + ) + assert written == [RESULT_FILE, OPTIONS_FILE, OVERVIEW_FILE] + assert (tmp_path / OVERVIEW_FILE).stat().st_size > 0 + saved = save.load_result(str(tmp_path / RESULT_FILE)) + np.testing.assert_allclose(saved["Ew_retr"], out.result.spectrum) + + +def test_session_files_append_uncertainty_to_the_result(tmp_path, session, monkeypatch): + options, out = session + appended = {} + monkeypatch.setattr( + save, "save_uncertainty", lambda est, path, force: appended.update(est) + ) + save_session(tmp_path, options, result=out.result, uncertainty={"m": "estimate"}) + assert appended == {"m": "estimate"} + + +def test_session_files_refuse_a_missing_folder_or_orphan_uncertainty(tmp_path, session): + options, _ = session + with pytest.raises(FileNotFoundError, match="no such folder"): + save_session(tmp_path / "absent", options) + with pytest.raises(ValueError, match="need the result"): + save_session(tmp_path, options, uncertainty={"m": object()}) + + +def test_session_files_accept_the_result_fitted_to_this_trace(tmp_path, session): + options, out = session + save_session(tmp_path, options, result=out.result, processed=out.processed) + saved = save.load_result(str(tmp_path / RESULT_FILE)) + assert result_mismatch(saved, out.tracedata, options.retrieve) == "" + del saved["omega"] # an older file without the axis: the trace still decides + assert result_mismatch(saved, out.tracedata, options.retrieve) == "" + + +@pytest.mark.parametrize( + ("change", "reason"), + [ + (lambda s: s.pop("trace_meas"), "does not store the trace"), + (lambda s: s.update(trace_meas=s["trace_meas"][:-1]), "grid but"), + (lambda s: s.update(trace_meas=1.5 * s["trace_meas"]), "no longer reproduces"), + (lambda s: s.update(omega=1.01 * s["omega"]), "different frequency grid"), + (lambda s: s.update(omega=s["omega"][:-1]), "different frequency grid"), + (lambda s: s.update(algorithm="no-such-solver"), "came from no-such-solver"), + ], +) +def test_session_files_refuse_a_result_fitted_to_other_data( + tmp_path, session, change, reason +): + options, out = session + save_session(tmp_path, options, result=out.result, processed=out.processed) + saved = save.load_result(str(tmp_path / RESULT_FILE)) + change(saved) + assert reason in result_mismatch(saved, out.tracedata, options.retrieve) + + +def test_format_options_matches_the_saved_file(tmp_path, session): + options, _ = session + save_session(tmp_path, options) + text = (tmp_path / OPTIONS_FILE).read_text(encoding="utf-8") + assert text == save.format_options(options.to_dict()) From 45f6bdc530e7f81a063a1ccb47d7401c0a5e42d0 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 21:57:00 +0000 Subject: [PATCH 06/41] Restyle the stages on the new sheet layout Every stage now builds on the workflow window's StagePage: a form of folding sections beside the plots, a one-line help that shows the focused control's tooltip, and the ready line in place of the status label. group() returns a Section holding a QFormLayout, so the stages' rows are unchanged and help still matches by (section, label). Retrieve's four advanced groups fold by default. The control-column splitter and its remembered width go; a narrow window moves the plots below the form instead. The old wizard takes the new style sheet and font until the window replaces it. Co-Authored-By: Claude Opus 5.5 --- croak/gui/base.py | 166 +++++------------------- croak/gui/canvas.py | 28 ++-- croak/gui/help.py | 66 +++------- croak/gui/settings.py | 23 ---- croak/gui/sheet.py | 206 +++++++++++++----------------- croak/gui/stage.py | 2 +- croak/gui/stage_dispersion.py | 1 - croak/gui/stage_load.py | 3 +- croak/gui/stage_marginal_check.py | 1 - croak/gui/stage_preprocess.py | 1 - croak/gui/stage_retrieve.py | 19 ++- croak/gui/stage_simulated.py | 3 +- croak/gui/stage_synthetic.py | 1 - croak/gui/stage_uncertainty.py | 1 - croak/gui/theme.py | 13 +- croak/gui/wizard.py | 3 + tests/gui/test_base.py | 44 ++++--- tests/gui/test_help.py | 6 +- tests/gui/test_sheet.py | 120 ++++++++--------- tests/gui/test_wizard_flow.py | 56 +++----- 20 files changed, 292 insertions(+), 471 deletions(-) diff --git a/croak/gui/base.py b/croak/gui/base.py index 715d212..6a76c5f 100644 --- a/croak/gui/base.py +++ b/croak/gui/base.py @@ -1,161 +1,63 @@ -"""Shared base for wizard stage pages (left controls + right plots).""" +"""The stage pages' wizard-era interface, on the workflow window's layout. -from __future__ import annotations - -from PySide6.QtCore import Qt, QTimer -from PySide6.QtGui import QShowEvent -from PySide6.QtWidgets import ( - QHBoxLayout, - QLabel, - QScrollArea, - QSplitter, - QVBoxLayout, - QWidget, -) - -from . import settings -from .help import HelpDialog, HelpSections, apply_tooltips, flatten -from .state import WizardState +:class:`Stage` is a :class:`~croak.gui.stage.StagePage` (form of folding +sections beside the plots) that keeps the calls the stage modules were written +against: ``controls`` to add sections to, :meth:`set_plot_area`, +:meth:`set_status` and the entry-page hooks the old wizard's footer reads. +""" -__all__ = ["Stage", "CONTROLS_MIN_WIDTH"] +from __future__ import annotations -#: Narrowest the control column can be dragged (logical px) before it snaps shut. -#: Wide enough for a label, a spin box and its unit on one form row. -CONTROLS_MIN_WIDTH = 240 -#: Debounce (ms) between a divider drag and remembering the new width. -_SAVE_WIDTH_DELAY_MS = 250 +from PySide6.QtWidgets import QWidget +from .help import apply_tooltips, flatten +from .stage import StagePage +from .state import WizardState -class Stage(QWidget): - """A wizard page: a scrollable control column on the left, plots on the right. +__all__ = ["Stage"] - The two sit on a :class:`~PySide6.QtWidgets.QSplitter`, so on a small display the - control column can be narrowed or dragged fully closed to give the plots the - room; its width is shared by every stage and remembered between launches - (:mod:`croak.gui.settings`). The plot side never collapses. - Subclasses build their controls into ``self.controls`` (a vertical layout) - and hand their plot widget to :meth:`set_plot_area`, then implement - :meth:`on_enter` to refresh when the page becomes visible. +class Stage(StagePage): + """A stage page: build into :attr:`controls`, then :meth:`set_plot_area`. - Subclasses may also set :attr:`HELP_TITLE`, :attr:`HELP_INTRO` and - :attr:`HELP` (see :mod:`croak.gui.help`) and call :meth:`apply_help` at the - end of ``__init__`` to attach option tooltips; :meth:`show_help` then opens - a summary dialog of the same content (wired to the wizard header's ``?``). + Subclasses add their sections (:func:`croak.gui.canvas.group`) to + ``self.controls``, hand their plots to :meth:`set_plot_area`, call + :meth:`apply_help` at the end of ``__init__`` and implement :meth:`on_enter`. """ - #: Short stage name shown in the help dialog title (e.g. ``"Retrieve"``). - HELP_TITLE: str = "" - #: One-paragraph overview shown at the top of the help dialog. - HELP_INTRO: str = "" - #: Per-option help; ``[(section, [(label, text), ...]), ...]``. - HELP: HelpSections = [] - - def __init__(self, state: WizardState): - super().__init__() - self.state = state - self._help_dialog: HelpDialog | None = None - - outer = QHBoxLayout(self) - outer.setContentsMargins(0, 0, 0, 0) - self.splitter = QSplitter(Qt.Orientation.Horizontal) - # left: scrollable controls - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setMinimumWidth(CONTROLS_MIN_WIDTH) - panel = QWidget() - self.controls = QVBoxLayout(panel) - scroll.setWidget(panel) - # right: plot area (filled by the subclass via set_plot_area) - self._right = QWidget() - self._right_layout = QVBoxLayout(self._right) - self._right_layout.setContentsMargins(0, 0, 0, 0) - self.splitter.addWidget(scroll) - self.splitter.addWidget(self._right) - # Dragging the divider fully left closes the controls; the plots never - # close. The stretch factors are load-bearing: without them QSplitter hands - # the spare width to the *left* pane ([1910, 6] at 1920 px) and squeezes - # the controls to their minimum whenever the window shrinks. - self.splitter.setCollapsible(0, True) - self.splitter.setCollapsible(1, False) - self.splitter.setStretchFactor(0, 0) - self.splitter.setStretchFactor(1, 1) - self.splitter.setSizes([settings.controls_width(), 1]) - # A drag fires splitterMoved continuously; remember the width once it rests. - self._save_width_timer = QTimer(self) - self._save_width_timer.setSingleShot(True) - self._save_width_timer.setInterval(_SAVE_WIDTH_DELAY_MS) - self._save_width_timer.timeout.connect(self._save_controls_width) - self.splitter.splitterMoved.connect(self._on_splitter_moved) - outer.addWidget(self.splitter) - - self.status_label = QLabel("") - self.status_label.setWordWrap(True) + def __init__(self, state: WizardState, *, find: bool = False): + super().__init__(state, find=find) + #: Where the sections go: the form's column. + self.controls = self.form.content + #: The form's ready line, which :meth:`set_status` writes. + self.status_label = self.form.ready def set_plot_area(self, plot: QWidget, *strips: QWidget) -> None: - """Fill the right-hand side: ``plot`` on top, then ``strips`` beneath it. - - ``plot`` takes every spare pixel; each strip (a read-out, a fold-away - panel) keeps its natural height. - """ - self._right_layout.addWidget(plot, stretch=1) + """Show ``plot`` (taking the room) with ``strips`` beneath it.""" + self.set_plots(plot) for strip in strips: - self._right_layout.addWidget(strip, stretch=0) - - def set_status(self, text: str) -> None: - self.status_label.setText(text) - - def showEvent(self, event: QShowEvent) -> None: # noqa: N802 — Qt override - """Apply the shared control-column width, so a drag on one stage reaches all.""" - super().showEvent(event) - width = settings.controls_width() - if self.splitter.sizes()[0] != width: - self.splitter.setSizes([width, 1]) + self.body.addWidget(strip, stretch=0) - def _on_splitter_moved(self, _pos: int, _index: int) -> None: - self._save_width_timer.start() - - def _save_controls_width(self) -> None: - settings.set_controls_width(self.splitter.sizes()[0]) + def set_status(self, text: str, state: str = "ok") -> None: + """Show ``text`` on the ready line, as ``"ok"``, ``"warn"`` or ``"error"``.""" + self.status_label.say(text, state) def apply_help(self) -> set[tuple[str, str]]: - """Attach :attr:`HELP` text as tooltips on the matching controls. + """Attach :attr:`HELP` as tooltips; return the ``(section, label)`` matched. - Call once at the end of the subclass ``__init__`` (after the controls - exist). Returns the matched ``(section, label)`` keys. + Call once at the end of the subclass ``__init__``, after the controls exist. """ return apply_tooltips(self, flatten(self.HELP)) - def show_help(self) -> HelpDialog | None: - """Open (or re-raise) the modeless summary dialog for this stage.""" - if not self.HELP: - return None - if self._help_dialog is None: - self._help_dialog = HelpDialog( - self.HELP_TITLE, self.HELP_INTRO, self.HELP, self - ) - self._help_dialog.show() - self._help_dialog.raise_() - self._help_dialog.activateWindow() - return self._help_dialog - - def on_enter(self) -> None: - """Called when this stage becomes the visible page (override).""" - # -- entry-page hooks --------------------------------------------------- - # The *entry pages* (the synthetic generator and the simulated-scan loader) + # The entry pages (the synthetic generator and the simulated-scan loader) # are reached from the welcome menu rather than by walking the linear stage - # sequence, so the wizard cannot decide from ``state.stage`` what its footer's - # "Next" button should say or do. They override these three hooks; the linear - # stages never use them (their Next is driven by ``WizardState.ready``). + # sequence, so the old wizard cannot decide from ``state.stage`` what its + # footer's "Next" button should say or do. They override these hooks. def next_label(self) -> str: """Text for the wizard footer's forward button on this page.""" return "Next →" - def can_advance(self) -> bool: - """Whether the forward button should be enabled on this page.""" - return True - def advance(self) -> None: """Do this page's forward action (generate/load, then move on).""" diff --git a/croak/gui/canvas.py b/croak/gui/canvas.py index 455284f..037ba1d 100644 --- a/croak/gui/canvas.py +++ b/croak/gui/canvas.py @@ -16,7 +16,6 @@ QComboBox, QDoubleSpinBox, QFormLayout, - QGroupBox, QSpinBox, QVBoxLayout, QWidget, @@ -25,6 +24,7 @@ from ..plotting import FULL_SCALE, PlotScale, plot_scale, scaled_rc from . import settings +from .sheet import Section __all__ = [ "MplCanvas", @@ -232,22 +232,18 @@ def collapsible( # -- tiny declarative widget builders --------------------------------------- -def group(title: str) -> tuple[QGroupBox, QFormLayout]: - """A titled group box with a form layout. - - The field-growth policy is set explicitly because ``QFormLayout``'s default is - style-dependent: the macOS style resolves it to ``FieldsStayAtSizeHint``, which - sizes each field to its *sizeHint* and so collapses the slider controls to the - width of their spin boxes. ``ExpandingFieldsGrow`` widens only the fields that - actually ask for the space (the ``Expanding`` size policy of - :class:`~croak.gui.widgets.RangeControl`/:class:`~croak.gui.widgets.ValueControl` - and of ``QLineEdit``), leaving spin boxes, combos and check boxes at their - natural size. +def group( + title: str, *, folded: bool = False, settings_key: str = "" +) -> tuple[Section, QFormLayout]: + """A folding section and the form layout its rows go in. + + The section is added to a stage's ``controls``; its rows are added to the + form with ``form.addRow(label, widget)``. Help tooltips match rows by + ``(title, label)`` (:func:`croak.gui.help.apply_tooltips`). ``folded`` and + ``settings_key`` are as for :class:`~croak.gui.sheet.Section`. """ - box = QGroupBox(title) - form = QFormLayout(box) - form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) - return box, form + section = Section(title, folded=folded, settings_key=settings_key) + return section, section.form def add_wide_row(form: QFormLayout, label: str, widget: QWidget) -> None: diff --git a/croak/gui/help.py b/croak/gui/help.py index 139f48f..43e6766 100644 --- a/croak/gui/help.py +++ b/croak/gui/help.py @@ -8,10 +8,11 @@ (:class:`HelpDialog`), and * the hover tooltip on the matching control, applied by :func:`apply_tooltips`. -Tooltips are matched by the control's ``(group-box title, form label)`` so that +Tooltips are matched by the control's ``(section title, form label)`` so that labels reused across groups (e.g. ``"λ unit"``) stay distinct. Widget labels spell -a literal ampersand ``&&`` (see :func:`_plain`), while :data:`Stage.HELP` keys are -written the way the user reads them, so the escape is undone before matching. +a literal ampersand ``&&`` (see :func:`croak.gui.sheet.plain`), while +:data:`Stage.HELP` keys are written the way the user reads them, so the escape is +undone before matching. """ from __future__ import annotations @@ -19,17 +20,16 @@ from html import escape from PySide6.QtWidgets import ( - QCheckBox, QDialog, QDialogButtonBox, QFormLayout, - QGroupBox, - QLabel, QTextBrowser, QVBoxLayout, QWidget, ) +from .sheet import Section, row_field, row_text + __all__ = ["HelpDialog", "HelpSections", "apply_tooltips", "build_html", "flatten"] #: A stage's help content: ordered ``(section_title, [(label, text), ...])``. @@ -82,41 +82,12 @@ def __init__( layout.addWidget(buttons) -def _plain(text: str) -> str: - """Undo Qt's mnemonic escaping, giving the label as the user reads it. - - Qt treats a lone ``&`` in a check box, push button, group-box title or form-row - label as a keyboard-mnemonic marker and does not draw it, so a *literal* - ampersand must be written ``&&``. Help keys are written unescaped, so strip the - escape before comparing. - - Examples - -------- - >>> _plain("Skip filtering && regrid") - 'Skip filtering & regrid' - """ - return text.replace("&&", "&") - - -def _field_widget(form: QFormLayout, row: int) -> QWidget | None: - """The widget in a form row, whether it occupies the field or spans both.""" - for role in (QFormLayout.ItemRole.FieldRole, QFormLayout.ItemRole.SpanningRole): - item = form.itemAt(row, role) - if item is not None and item.widget() is not None: - return item.widget() - return None - - -def _row_label(form: QFormLayout, row: int, field: QWidget) -> str | None: - """Label text for a form row: the label column, else a checkbox's own text.""" - item = form.itemAt(row, QFormLayout.ItemRole.LabelRole) - if item is not None: - label = item.widget() - if isinstance(label, QLabel): - return _plain(label.text()) - if isinstance(field, QCheckBox): - return _plain(field.text()) - return None +def _section_title(form: QFormLayout) -> str: + """The title of the section a form row sits in ("" outside any section).""" + widget = form.parentWidget() + while widget is not None and not isinstance(widget, Section): + widget = widget.parentWidget() + return widget.section_title if widget is not None else "" def apply_tooltips( @@ -125,20 +96,17 @@ def apply_tooltips( """Set tooltips on ``root``'s form controls from ``help_map``. Walks every :class:`QFormLayout` under ``root`` and, for each row, looks up - ``(enclosing group-box title, row label)`` in ``help_map``; on a hit the + ``(enclosing section title, row label)`` in ``help_map``; on a hit the field widget's tooltip is set. Returns the set of keys that matched, so callers (and tests) can detect help entries with no corresponding control. """ matched: set[tuple[str, str]] = set() for form in root.findChildren(QFormLayout): - box = form.parentWidget() - section = _plain(box.title()) if isinstance(box, QGroupBox) else "" + section = _section_title(form) for row in range(form.rowCount()): - field = _field_widget(form, row) - if field is None: - continue - label = _row_label(form, row, field) - if label is None: + field = row_field(form, row) + label = row_text(form, row) + if field is None or not label: continue key = (section, label) text = help_map.get(key) diff --git a/croak/gui/settings.py b/croak/gui/settings.py index a3c5f90..1d9ad59 100644 --- a/croak/gui/settings.py +++ b/croak/gui/settings.py @@ -20,7 +20,6 @@ from __future__ import annotations import json -from typing import cast from PySide6.QtCore import QByteArray, QSettings, QSize from PySide6.QtGui import QGuiApplication @@ -31,16 +30,12 @@ __all__ = [ "KEY_WINDOW_GEOMETRY", - "KEY_CONTROLS_WIDTH", "DEFAULT_WINDOW_SIZE", - "DEFAULT_CONTROLS_WIDTH", "SCREEN_FILL", "user_settings", "fit_to_screen", "restore_window", "save_window", - "controls_width", - "set_controls_width", "value", "set_value", "text", @@ -55,13 +50,9 @@ #: Settings key holding the recently saved or opened sessions (``options.toml`` #: paths, newest first) as JSON text. KEY_RECENT_SESSIONS = "home/recent_sessions" -#: Settings key holding the control column's width in logical pixels. -KEY_CONTROLS_WIDTH = "stage/controls_width" #: First-run window size (logical px) before clamping to the screen; large enough #: for the plot pages at full text density on a 1920x1080 display. DEFAULT_WINDOW_SIZE = QSize(1500, 950) -#: First-run width (logical px) of the control column on every stage. -DEFAULT_CONTROLS_WIDTH = 315 #: Fraction of the screen's available area a first-run window may fill. SCREEN_FILL = 0.9 @@ -136,20 +127,6 @@ def save_window(window: QWidget) -> None: store.sync() -def controls_width() -> int: - """The remembered control-column width (logical px), else the default.""" - # ``type=int`` makes Qt return an int; PySide6's stubs only say ``object``. - stored = user_settings().value(KEY_CONTROLS_WIDTH, DEFAULT_CONTROLS_WIDTH, int) - return int(cast(int, stored)) - - -def set_controls_width(px: int) -> None: - """Remember the control-column width (logical px) for every stage and launch.""" - store = user_settings() - store.setValue(KEY_CONTROLS_WIDTH, int(px)) - store.sync() - - def value(key: str, default: bool) -> bool: """Read a remembered flag (e.g. whether a strip is expanded).""" return bool(user_settings().value(key, default, bool)) diff --git a/croak/gui/sheet.py b/croak/gui/sheet.py index 6970a87..b4c3f0f 100644 --- a/croak/gui/sheet.py +++ b/croak/gui/sheet.py @@ -1,27 +1,28 @@ """The building blocks of a stage: a form of folding sections beside its plots. A :class:`Sheet` is the layout every stage shares: a :class:`Form` on the left -(sections of rows, the one-line help for the focused row, and a -:class:`ReadyLine`) and the stage's plots on the right, which move below the +(folding :class:`Section` s of rows, a one-line help for the focused control and +a :class:`ReadyLine`) and the stage's plots on the right, which move below the form when the window is narrow. -Help text comes from the stage's ``HELP`` sections (:mod:`croak.gui.help`): -a row added to section ``S`` with label ``L`` takes ``HELP[(S, L)]`` as its -tooltip and as the help line shown while it has focus; a row whose editor is a -check box is keyed by the box's text. :attr:`Form.matched` records the keys that -found a row, so a test can check that no help entry is orphaned. +A section's rows live in an ordinary :class:`~PySide6.QtWidgets.QFormLayout` +(:attr:`Section.form`), so a stage adds them with ``form.addRow(label, widget)``. +Help text is attached afterwards by :func:`croak.gui.help.apply_tooltips`, keyed +by ``(section title, row label)``; the one-line help under the form shows the +tooltip of whatever has focus, so the tooltip is the help's single source. """ from __future__ import annotations -from collections.abc import Mapping, Sequence +from collections.abc import Sequence from typing import cast -from PySide6.QtCore import QEvent, QObject, Qt, Signal +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QApplication, QBoxLayout, QCheckBox, + QFormLayout, QFrame, QGridLayout, QHBoxLayout, @@ -37,7 +38,6 @@ ) from . import settings -from .help import HelpSections, flatten from .theme import label, set_flag __all__ = [ @@ -46,15 +46,14 @@ "Form", "ProgressStrip", "ReadyLine", - "Row", "Section", "Segments", "Sheet", + "plain", + "row_field", + "row_text", ] -LABEL_WIDTH = 124 -"""The width of a row's label column, logical px.""" - NARROW_WIDTH = 1000 """Below this window width (logical px) a sheet's plots move under its form.""" @@ -65,6 +64,10 @@ def plain(text: str) -> str: """Undo Qt's ``&&`` escape, giving a label as the user reads it. + Qt reads a lone ``&`` in a check box, button or section title as a keyboard + mnemonic, so a literal ampersand is written ``&&``; help keys are written + the way the user reads them. + Examples -------- >>> plain("Skip filtering && regrid") @@ -73,55 +76,37 @@ def plain(text: str) -> str: return text.replace("&&", "&") -class Row(QWidget): - """One setting: a label in the label column beside its editor. +def row_text(form: QFormLayout, row: int) -> str: + """The label of ``form``'s row ``row``: its label, else a check box's own text. - A check box carries its own text and takes the whole width. + A row with neither (a button row, a note) has no label: ``""``. """ + item = form.itemAt(row, QFormLayout.ItemRole.LabelRole) + label_widget = item.widget() if item is not None else None + if isinstance(label_widget, QLabel): + return plain(label_widget.text()) + field = row_field(form, row) + return plain(field.text()) if isinstance(field, QCheckBox) else "" - def __init__(self, text: str, editor: QWidget, help_text: str = "") -> None: - super().__init__() - self.editor = editor - self.help_text = help_text - self.text = plain(editor.text()) if isinstance(editor, QCheckBox) else text - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 2, 0, 2) - layout.setSpacing(8) - self.label: QLabel | None = None - if not isinstance(editor, QCheckBox): - self.label = label(text, "rowLabel") - self.label.setFixedWidth(LABEL_WIDTH) - self.label.setWordWrap(True) - layout.addWidget(self.label) - layout.addWidget(editor, stretch=1) - if help_text: - editor.setToolTip(help_text) - if self.label is not None: - self.label.setToolTip(help_text) - - def changeEvent(self, event: QEvent) -> None: # noqa: N802 — Qt override - # The label greys with its editor: a disabled row reads as unavailable. - if event.type() == QEvent.Type.EnabledChange and self.label is not None: - self.label.setEnabled(self.isEnabled()) - super().changeEvent(event) - - def matches(self, query: str) -> bool: - """Whether ``query`` (case-insensitive) is in this row's label or help.""" - query = query.casefold() - return query in self.text.casefold() or query in self.help_text.casefold() + +def row_field(form: QFormLayout, row: int) -> QWidget | None: + """The widget in ``form``'s row ``row``: its field, or one spanning the row.""" + for role in (QFormLayout.ItemRole.FieldRole, QFormLayout.ItemRole.SpanningRole): + item = form.itemAt(row, role) + widget = item.widget() if item is not None else None + if widget is not None: + return widget + return None class Section(QWidget): - """A titled group of rows that folds open and shut in place. + """A titled group of form rows that folds open and shut in place. Parameters ---------- title : str - Its heading, and the first half of its rows' help keys. - help_map : mapping - ``(section, label) -> text``, from the stage's ``HELP``. - matched : set - Where the help keys this section uses are recorded. + Its heading (``&&`` for a literal ampersand); the first half of its + rows' help keys. folded : bool Its initial state; a remembered state (``settings_key``) wins. settings_key : str @@ -131,24 +116,15 @@ class Section(QWidget): toggled = Signal(bool) def __init__( - self, - title: str, - help_map: Mapping[tuple[str, str], str], - matched: set[tuple[str, str]], - *, - folded: bool = False, - settings_key: str = "", + self, title: str, *, folded: bool = False, settings_key: str = "" ) -> None: super().__init__() - self.title = title - self.rows: list[Row] = [] - self._help = help_map - self._matched = matched + self.section_title = plain(title) self._key = settings_key if settings_key: folded = not settings.value(settings_key, not folded) outer = QVBoxLayout(self) - outer.setContentsMargins(0, 0, 0, 6) + outer.setContentsMargins(0, 0, 0, 8) outer.setSpacing(0) self.header = QToolButton() self.header.setObjectName("sectionHeader") @@ -160,15 +136,24 @@ def __init__( self.header.toggled.connect(self._fold) outer.addWidget(self.header) self.body = QWidget() - self.layout_ = QVBoxLayout(self.body) - self.layout_.setContentsMargins(14, 0, 0, 2) - self.layout_.setSpacing(2) + self.form = QFormLayout(self.body) + self.form.setContentsMargins(14, 2, 0, 2) + self.form.setHorizontalSpacing(10) + self.form.setVerticalSpacing(6) + self.form.setLabelAlignment( + Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter + ) + # Grow only the fields that ask for room (sliders, paths): the macOS + # style's default would size every field to its hint instead. + self.form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow + ) outer.addWidget(self.body) self._fold(not folded, remember=False) @property def folded(self) -> bool: - """Whether the body is hidden.""" + """Whether the rows are hidden.""" return not self.header.isChecked() def set_folded(self, folded: bool) -> None: @@ -184,21 +169,20 @@ def _fold(self, open_: bool, remember: bool = True) -> None: settings.set_value(self._key, open_) self.toggled.emit(open_) - def add_row(self, text: str, editor: QWidget) -> Row: - """Add a setting labelled ``text`` (ignored for a check box) and return it.""" - key_label = plain(editor.text()) if isinstance(editor, QCheckBox) else text - key = (self.title, key_label) - help_text = self._help.get(key, "") - if help_text: - self._matched.add(key) - row = Row(text, editor, help_text) - self.rows.append(row) - self.layout_.addWidget(row) - return row + def filter(self, query: str) -> int: + """Show only the rows whose label or help contains ``query``; count them. - def add_widget(self, widget: QWidget) -> None: - """Add a widget across the section's width (a button row, a table, …).""" - self.layout_.addWidget(widget) + An empty ``query`` shows every row. + """ + query = query.casefold() + hits = 0 + for row in range(self.form.rowCount()): + field = row_field(self.form, row) + tip = field.toolTip() if field is not None else "" + hit = not query or query in f"{row_text(self.form, row)}\n{tip}".casefold() + self.form.setRowVisible(row, hit) + hits += hit + return hits class Form(QFrame): @@ -206,21 +190,14 @@ class Form(QFrame): Parameters ---------- - help_sections : HelpSections - The stage's ``HELP``. find : bool Offer "Find a setting", which shows only the rows that match. """ - def __init__( - self, help_sections: HelpSections | None = None, *, find: bool = False - ) -> None: + def __init__(self, *, find: bool = False) -> None: super().__init__() self.setObjectName("form") self.setMinimumWidth(FORM_MIN_WIDTH) - self.help_map = flatten(help_sections or []) - self.matched: set[tuple[str, str]] = set() - self.sections: list[Section] = [] outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) outer.setSpacing(0) @@ -243,8 +220,7 @@ def __init__( content = QWidget() self.content = QVBoxLayout(content) self.content.setContentsMargins(18, 10, 14, 10) - self.content.setSpacing(4) - self.content.addStretch(1) + self.content.setSpacing(2) scroll.setWidget(content) outer.addWidget(scroll, stretch=1) foot = QWidget() @@ -263,46 +239,40 @@ def __init__( self._focus_moved ) + @property + def sections(self) -> list[Section]: + """The form's sections, in order.""" + return self.findChildren(Section) + def add_section( self, title: str, *, folded: bool = False, settings_key: str = "" ) -> Section: """Append a section and return it.""" - section = Section( - title, - self.help_map, - self.matched, - folded=folded, - settings_key=settings_key, - ) - self.sections.append(section) - self.content.insertWidget(self.content.count() - 1, section) + section = Section(title, folded=folded, settings_key=settings_key) + self.content.addWidget(section) return section def add_widget(self, widget: QWidget) -> None: - """Append a widget outside any section (a heading, a file picker, …).""" - self.content.insertWidget(self.content.count() - 1, widget) + """Append a widget outside any section (a button row, a note, …).""" + self.content.addWidget(widget) def _focus_moved(self, _old: QWidget | None, new: QWidget | None) -> None: - widget: QObject | None = new - while widget is not None and not isinstance(widget, Row): - widget = widget.parent() - if isinstance(widget, Row) and self.isAncestorOf(widget): - self.help.setText(widget.help_text) + """Show the help of the newly focused control, if it is on this form.""" + widget = new + while widget is not None and widget is not self: + if widget.toolTip(): + if self.isAncestorOf(widget): + self.help.setText(widget.toolTip()) + return + widget = widget.parentWidget() def filter(self, query: str) -> None: """Show only the rows whose label or help contains ``query``.""" query = query.strip() for section in self.sections: - hits = 0 - for row in section.rows: - hit = not query or row.matches(query) - row.setVisible(hit) - hits += hit + hits = section.filter(query) section.setVisible(not query or hits > 0) - if query and hits: - section.body.setVisible(True) - elif not query: - section.body.setVisible(not section.folded) + section.body.setVisible(bool(query and hits) or not section.folded) class Sheet(QWidget): diff --git a/croak/gui/stage.py b/croak/gui/stage.py index 76b1772..ddafebc 100644 --- a/croak/gui/stage.py +++ b/croak/gui/stage.py @@ -47,7 +47,7 @@ def __init__(self, state: WizardState, *, find: bool = False) -> None: super().__init__() self.state = state self._help_dialog: HelpDialog | None = None - self.form = Form(self.HELP, find=find) + self.form = Form(find=find) self.advice = AdviceBox() self.strip = ProgressStrip() self.derived = DerivedGrid() diff --git a/croak/gui/stage_dispersion.py b/croak/gui/stage_dispersion.py index 6ebc048..8964fdb 100644 --- a/croak/gui/stage_dispersion.py +++ b/croak/gui/stage_dispersion.py @@ -256,7 +256,6 @@ def __init__(self, state): bl.addWidget(reset) bl.addWidget(save_btn) self.controls.addWidget(btns) - self.controls.addWidget(self.status_label) self.controls.addStretch(1) self.canvas = MplCanvas(figsize=(9, 7)) diff --git a/croak/gui/stage_load.py b/croak/gui/stage_load.py index d3117ad..fe9385e 100644 --- a/croak/gui/stage_load.py +++ b/croak/gui/stage_load.py @@ -339,7 +339,7 @@ def __init__(self, state): ) self.controls.addWidget(box) - # Units & interaction ("&&" escapes the literal ampersand; see help._plain) + # Units & interaction ("&&" escapes the literal ampersand; see sheet.plain) box, form = group("Units && interaction") self.lam_unit_combo = combo( _LAM_UNITS, p.lam_unit, lambda v: setattr(p, "lam_unit", v) @@ -454,7 +454,6 @@ def __init__(self, state): load_btn = QPushButton("Load && Preview") load_btn.clicked.connect(self._load_preview) self.controls.addWidget(load_btn) - self.controls.addWidget(self.status_label) self.controls.addStretch(1) self.trace_canvas = MplCanvas() diff --git a/croak/gui/stage_marginal_check.py b/croak/gui/stage_marginal_check.py index ba4ad2d..a1f7b76 100644 --- a/croak/gui/stage_marginal_check.py +++ b/croak/gui/stage_marginal_check.py @@ -212,7 +212,6 @@ def __init__(self, state, wizard): form.addRow(self.marginal_correct_check) self.controls.addWidget(box) - self.controls.addWidget(self.status_label) self.controls.addStretch(1) # Trace above, marginal comparison below, on a draggable divider so a diff --git a/croak/gui/stage_preprocess.py b/croak/gui/stage_preprocess.py index f43263b..2349160 100644 --- a/croak/gui/stage_preprocess.py +++ b/croak/gui/stage_preprocess.py @@ -407,7 +407,6 @@ def __init__(self, state): ) self.reset_btn.clicked.connect(self._reset_defaults) self.controls.addWidget(self.reset_btn) - self.controls.addWidget(self.status_label) self.controls.addStretch(1) self.canvas = MplCanvas(figsize=(9, 5)) diff --git a/croak/gui/stage_retrieve.py b/croak/gui/stage_retrieve.py index 927ca51..a183e94 100644 --- a/croak/gui/stage_retrieve.py +++ b/croak/gui/stage_retrieve.py @@ -594,7 +594,11 @@ def __init__(self, state): form.addRow("Quadrature nodes", self.smear_nodes_spin) self.controls.addWidget(box) - self.focal_box, form = group("Chromatic focal mixture (advanced)") + self.focal_box, form = group( + "Chromatic focal mixture (advanced)", + folded=True, + settings_key="retrieve/open_focal", + ) box = self.focal_box self.focal_check = check( "Enable focal mixture", @@ -706,7 +710,9 @@ def __init__(self, state): self.collection_diam_spin.setEnabled(p.collection == "manual") self.controls.addWidget(box) - self.extra_box, form = group("Extra fit parameters") + self.extra_box, form = group( + "Extra fit parameters", folded=True, settings_key="retrieve/open_extra" + ) box = self.extra_box self.fit_thickness_check = check( "Fit medium thickness", @@ -762,7 +768,9 @@ def __init__(self, state): form.addRow("Delay origin", self.delay_origin_combo) self.controls.addWidget(box) - self.reg_box, form = group("Regularisation") + self.reg_box, form = group( + "Regularisation", folded=True, settings_key="retrieve/open_regularisation" + ) box = self.reg_box # Scientific-notation spin boxes (decade-aware stepping) so weights well # below 0.001 — common with LM — can be entered, e.g. 5e-4. @@ -802,7 +810,9 @@ def __init__(self, state): form.addRow("τ window hi (fs)", self.reg_time_hi_spin) self.controls.addWidget(box) - self.global_box, form = group("Global search (cma-es)") + self.global_box, form = group( + "Global search (cma-es)", folded=True, settings_key="retrieve/open_global" + ) box = self.global_box self.strategy_combo = combo( ["cma", "sep-cma", "de"], @@ -878,7 +888,6 @@ def __init__(self, state): self.retarget_btn.clicked.connect(self._retarget) self.retarget_btn.setEnabled(False) self.controls.addWidget(self.retarget_btn) - self.controls.addWidget(self.status_label) self.controls.addStretch(1) # Figure on top (it takes every spare pixel), numeric read-out beneath it diff --git a/croak/gui/stage_simulated.py b/croak/gui/stage_simulated.py index 430ae71..7c262eb 100644 --- a/croak/gui/stage_simulated.py +++ b/croak/gui/stage_simulated.py @@ -281,7 +281,7 @@ def __init__(self, state, wizard): add_wide_row(form, "load λ band (nm)", self.band) # Skip the marginal-check + preprocess stages and load the as-simulated # ω-density trace on its native grid straight into the retrieval. "&&" is - # Qt's escape for a literal ampersand in a widget label (see help._plain). + # Qt's escape for a literal ampersand in a widget label (see sheet.plain). self.raw_direct = check( "Skip filtering && regrid (raw → retrieval)", self._defaults.raw_direct, @@ -292,7 +292,6 @@ def __init__(self, state, wizard): # Navigation (Back / "Load & continue →") and Help live in the shared # wizard chrome, via the entry-page hooks below. - self.controls.addWidget(self.status_label) self.controls.addStretch(1) self.canvas = MplCanvas(figsize=(9, 7)) diff --git a/croak/gui/stage_synthetic.py b/croak/gui/stage_synthetic.py index 6405f3a..d3f9f5a 100644 --- a/croak/gui/stage_synthetic.py +++ b/croak/gui/stage_synthetic.py @@ -174,7 +174,6 @@ def __init__(self, state, wizard): # Navigation (Back / "Generate →") lives in the shared wizard footer, via # the entry-page hooks below. - self.controls.addWidget(self.status_label) self.controls.addStretch(1) self._seed_sampling() diff --git a/croak/gui/stage_uncertainty.py b/croak/gui/stage_uncertainty.py index ed0be78..f8ee17e 100644 --- a/croak/gui/stage_uncertainty.py +++ b/croak/gui/stage_uncertainty.py @@ -162,7 +162,6 @@ def __init__(self, state): self.controls.addWidget(est_box) self.controls.addStretch(1) - self.controls.addWidget(self.status_label) self._sync_enabled() self.canvas = MplCanvas(figsize=(11, 4.5)) diff --git a/croak/gui/theme.py b/croak/gui/theme.py index cf7a32c..7206012 100644 --- a/croak/gui/theme.py +++ b/croak/gui/theme.py @@ -207,8 +207,19 @@ def stylesheet(p: Palette = PALETTE) -> str: f" background: {p.background}; color: {p.ink}; outline: 0;" f" selection-background-color: {p.selected_bg};" f" selection-color: {p.selected_ink}; }}", + # Spin boxes step with the wheel and the arrow keys; the arrow buttons + # are hidden so a spin box is the same height and look as a combo. "QAbstractSpinBox::up-button, QAbstractSpinBox::down-button" - " { width: 0; border: 0; }", + " { width: 0; height: 0; border: 0; }", + "QLineEdit, QComboBox, QAbstractSpinBox { min-height: 18px; }", + # thin scroll bars that do not compete with the content + "QScrollBar:vertical { width: 10px; background: transparent; margin: 0; }", + "QScrollBar:horizontal { height: 10px; background: transparent; margin: 0; }", + f"QScrollBar::handle {{ background: {p.track}; border-radius: 4px;" + " min-height: 24px; min-width: 24px; margin: 2px; }", + f"QScrollBar::handle:hover {{ background: {p.border}; }}", + "QScrollBar::add-line, QScrollBar::sub-line { width: 0; height: 0; }", + "QScrollBar::add-page, QScrollBar::sub-page { background: none; }", f"QCheckBox {{ color: {p.ink}; spacing: 6px; }}", f"QCheckBox:disabled {{ color: {p.disabled}; }}", f"QLabel#derivedLabel {{ color: {p.muted}; font-size: {SMALL_PX}px; }}", diff --git a/croak/gui/wizard.py b/croak/gui/wizard.py index fdfce09..bc6d38e 100644 --- a/croak/gui/wizard.py +++ b/croak/gui/wizard.py @@ -52,6 +52,7 @@ from .stage_uncertainty import StageUncertainty from .stage_welcome import StageWelcome from .state import WizardState +from .theme import load_fonts, stylesheet _TITLES = [ "1. Load", @@ -77,6 +78,8 @@ def __init__(self, state: WizardState | None = None): # it opens; setting it here too means an embedder who built their own # QApplication still gets the icon. self.setWindowIcon(app_icon()) + load_fonts() + self.setStyleSheet(stylesheet()) # Un-maximised size: as the user last left it, else a default clamped to # the screen (run_wizard maximises on top of this). settings.restore_window(self) diff --git a/tests/gui/test_base.py b/tests/gui/test_base.py index d91ccdc..a619fb8 100644 --- a/tests/gui/test_base.py +++ b/tests/gui/test_base.py @@ -1,4 +1,4 @@ -"""The Stage base page: help dialog, entry-page hooks and the shared width.""" +"""The Stage base page: help dialog, entry-page hooks and the sheet layout.""" from __future__ import annotations @@ -6,8 +6,10 @@ pytest.importorskip("PySide6") -from croak.gui import settings +from PySide6.QtWidgets import QLabel, QLineEdit + from croak.gui.base import Stage +from croak.gui.canvas import group from croak.gui.state import WizardState @@ -41,19 +43,27 @@ def test_stage_default_hooks(qtbot): assert stage.on_enter() is None -def test_stage_shows_with_the_shared_controls_width(qtbot): - settings.set_controls_width(300) - stage = Stage(WizardState()) +def test_stage_builds_on_the_sheet_layout(qtbot): + stage = _HelpedStage(WizardState()) + qtbot.addWidget(stage) + # Sections go in the form's column; the ready line is the status. + assert stage.controls is stage.form.content + assert stage.status_label is stage.form.ready + stage.set_status("Loaded.") + assert stage.form.ready.text() == "Loaded." and stage.form.ready.state == "ok" + stage.set_status("Failed: no file", "error") + assert stage.form.ready.property("error") is True + plot, strip = QLabel("plot"), QLabel("strip") + stage.set_plot_area(plot, strip) + assert stage.isAncestorOf(plot) and stage.isAncestorOf(strip) + + +def test_stage_help_reaches_the_section_rows(qtbot): + stage = _HelpedStage(WizardState()) qtbot.addWidget(stage) - stage.resize(1200, 600) - stage.show() - qtbot.waitExposed(stage) - assert stage.splitter.sizes()[0] == 300 - # Showing again with the width already applied changes nothing. - stage.hide() - stage.show() - assert stage.splitter.sizes()[0] == 300 - # A rested divider drag is remembered for every stage. - stage.splitter.setSizes([280, 900]) - stage._save_controls_width() - assert settings.controls_width() == 280 + section, form = group("Section") + field = QLineEdit() + form.addRow("Label", field) + stage.controls.addWidget(section) + assert stage.apply_help() == {("Section", "Label")} + assert field.toolTip() == "Text" diff --git a/tests/gui/test_help.py b/tests/gui/test_help.py index 0bae037..b5a8a27 100644 --- a/tests/gui/test_help.py +++ b/tests/gui/test_help.py @@ -9,7 +9,6 @@ from PySide6.QtWidgets import ( QCheckBox, QFormLayout, - QGroupBox, QHBoxLayout, QLineEdit, QPushButton, @@ -18,6 +17,7 @@ ) from croak.gui.help import HelpDialog, apply_tooltips, build_html, flatten +from croak.gui.sheet import Section def test_build_html_escapes_and_skips_an_empty_intro(): @@ -41,8 +41,8 @@ def test_apply_tooltips_skips_rows_without_a_usable_field_or_label(qtbot): root = QWidget() qtbot.addWidget(root) outer = QVBoxLayout(root) - box = QGroupBox("Group && more") - form = QFormLayout(box) + box = Section("Group && more") + form = box.form labelled = QLineEdit() form.addRow("Plain", labelled) # A layout (not a widget) in the field column: no field widget, skipped. diff --git a/tests/gui/test_sheet.py b/tests/gui/test_sheet.py index 8c0a4c6..46a5ab4 100644 --- a/tests/gui/test_sheet.py +++ b/tests/gui/test_sheet.py @@ -1,4 +1,4 @@ -"""Sheet building blocks: sections, rows, help, find, reflow and status widgets.""" +"""Sheet building blocks: sections, help, find, reflow and status widgets.""" from __future__ import annotations @@ -9,87 +9,89 @@ from PySide6.QtWidgets import QCheckBox, QLabel, QLineEdit, QSpinBox, QWidget from croak.gui import settings +from croak.gui.help import apply_tooltips from croak.gui.sheet import ( - LABEL_WIDTH, NARROW_WIDTH, AdviceBox, DerivedGrid, Form, ProgressStrip, ReadyLine, - Row, + Section, Segments, Sheet, + row_text, ) -HELP = [ - ("Grid", [("Points", "How many samples."), ("Resample t", "Resample in time.")]), - ("Filtering", [("Threshold", "Clip below this.")]), - ("Unused", [("Nothing", "No control has this label.")]), -] +HELP = { + ("Grid", "Points"): "How many samples.", + ("Grid", "Resample t"): "Resample in time.", + ("Filtering", "Threshold"): "Clip below this.", +} def _form(qtbot, **kwargs) -> Form: - form = Form(HELP, **kwargs) + form = Form(**kwargs) qtbot.addWidget(form) return form -def test_sheet_rows_take_their_help_from_section_and_label(qtbot): - form = _form(qtbot) +def _grid(form: Form): grid = form.add_section("Grid") - points = grid.add_row("Points", QSpinBox()) - resample = grid.add_row("ignored", QCheckBox("Resample t")) - loose = grid.add_row("Undocumented", QLineEdit()) - assert points.editor.toolTip() == "How many samples." - assert points.label is not None and points.label.width() == LABEL_WIDTH - # A check box is its own label and is keyed by its text. - assert resample.label is None - assert resample.text == "Resample t" - assert resample.editor.toolTip() == "Resample in time." - assert loose.editor.toolTip() == "" - assert form.matched == {("Grid", "Points"), ("Grid", "Resample t")} - - -def test_sheet_check_box_with_escaped_ampersand_finds_its_help(qtbot): - form = Form([("S", [("Skip filtering & regrid", "Raw.")])]) - qtbot.addWidget(form) - row = form.add_section("S").add_row("", QCheckBox("Skip filtering && regrid")) - assert row.help_text == "Raw." - - -def test_sheet_row_label_greys_with_its_row(qtbot): - row = Row("Points", QSpinBox()) - qtbot.addWidget(row) - row.setEnabled(False) - assert row.label is not None and not row.label.isEnabled() - row.setEnabled(True) - assert row.label.isEnabled() - box = Row("", QCheckBox("Flag")) - qtbot.addWidget(box) - box.setEnabled(False) # a check box row has no separate label to grey + points = QSpinBox() + resample = QCheckBox("Resample t") + grid.form.addRow("Points", points) + grid.form.addRow(resample) + return grid, points, resample + + +def test_sheet_section_rows_take_help_by_section_and_label(qtbot): + form = _form(qtbot) + _, points, resample = _grid(form) + loose = QLineEdit() + form.add_section("Other").form.addRow("Undocumented", loose) + matched = apply_tooltips(form, HELP) + assert matched == {("Grid", "Points"), ("Grid", "Resample t")} + assert points.toolTip() == "How many samples." + assert resample.toolTip() == "Resample in time." + assert loose.toolTip() == "" -def test_sheet_focused_row_shows_its_help_line(qtbot): +def test_sheet_section_title_reads_an_escaped_ampersand(qtbot): + section = Section("Units && interaction") + qtbot.addWidget(section) + assert section.section_title == "Units & interaction" + assert section.header.text() == "Units && interaction" + + +def test_sheet_row_text_reads_a_label_or_a_check_box(qtbot): + form = _form(qtbot) + grid, _, _ = _grid(form) + grid.form.addRow(QWidget()) # neither: no text + assert [row_text(grid.form, r) for r in range(3)] == ["Points", "Resample t", ""] + + +def test_sheet_focused_control_shows_its_help_line(qtbot): form = _form(qtbot) form.show() qtbot.waitExposed(form) - spin = QSpinBox() - form.add_section("Grid").add_row("Points", spin) - spin.setFocus() + _, points, _ = _grid(form) + apply_tooltips(form, HELP) + points.setFocus() qtbot.waitUntil(lambda: form.help.text() == "How many samples.") - other = QLineEdit() - form.add_widget(other) # outside any row: the help line keeps its text - other.setFocus() - qtbot.waitUntil(other.hasFocus) + plain = QLineEdit() # no tooltip anywhere up to the form: help unchanged + form.add_widget(plain) + plain.setFocus() + qtbot.waitUntil(plain.hasFocus) assert form.help.text() == "How many samples." -def test_sheet_help_ignores_rows_of_another_form(qtbot): +def test_sheet_help_ignores_controls_of_another_form(qtbot): form, other = _form(qtbot), _form(qtbot) - spin = QSpinBox() - other.add_section("Grid").add_row("Points", spin) - form._focus_moved(None, spin) + elsewhere = QSpinBox() + elsewhere.setToolTip("Not mine.") + other.add_widget(elsewhere) + form._focus_moved(None, elsewhere) assert form.help.text() == "" form._focus_moved(None, None) @@ -106,18 +108,18 @@ def test_sheet_sections_fold_and_remember_it(qtbot): assert not again.folded # the remembered state wins over the default plain = form.add_section("Filtering") plain.set_folded(True) # no key: nothing remembered - plain.add_widget(QLabel("extra")) assert plain.folded + assert form.sections == [section, plain] def test_sheet_find_shows_only_matching_rows(qtbot): form = _form(qtbot, find=True) form.show() - grid = form.add_section("Grid") - points = grid.add_row("Points", QSpinBox()) - resample = grid.add_row("", QCheckBox("Resample t")) + grid, points, resample = _grid(form) filtering = form.add_section("Filtering", folded=True) - threshold = filtering.add_row("Threshold", QSpinBox()) + threshold = QSpinBox() + filtering.form.addRow("Threshold", threshold) + apply_tooltips(form, HELP) assert form.find is not None form.find.setText("clip") # matches the threshold's help text assert threshold.isVisible() and filtering.body.isVisible() @@ -133,7 +135,7 @@ def test_sheet_form_without_find_has_no_box(qtbot): form = Form() qtbot.addWidget(form) assert form.find is None - assert form.help_map == {} + assert form.sections == [] def test_sheet_plots_move_below_the_form_on_a_narrow_window(qtbot): diff --git a/tests/gui/test_wizard_flow.py b/tests/gui/test_wizard_flow.py index 554400a..7b35f96 100644 --- a/tests/gui/test_wizard_flow.py +++ b/tests/gui/test_wizard_flow.py @@ -24,7 +24,6 @@ from PySide6.QtCore import QSize, Qt from croak.gui import settings -from croak.gui.base import CONTROLS_MIN_WIDTH from croak.gui.canvas import MplCanvas, collapsible from croak.gui.stage_load import assemble_load_data, seed_preproc_defaults from croak.gui.stage_marginal_check import StageMarginalCheck @@ -1835,7 +1834,6 @@ def test_slider_controls_fill_the_control_column(qtbot): w.resize(1500, 950) w.show() qtbot.waitExposed(w) - assert stage.splitter.sizes()[0] == settings.DEFAULT_CONTROLS_WIDTH box = stage.lam_range.parentWidget() assert box is not None @@ -1848,21 +1846,22 @@ def test_widget_labels_escape_literal_ampersands(qtbot): """A literal "&" in a widget label is written "&&", and help still matches. Regression: Qt reads a lone ``&`` in a check box, push button, radio button, - group-box title or form-row label as a keyboard-mnemonic marker and does not + section title or form-row label as a keyboard-mnemonic marker and does not draw it, so "Skip filtering & regrid" rendered as "Skip filtering regrid". - Escaping it as ``&&`` fixes the rendering, and :func:`croak.gui.help._plain` + Escaping it as ``&&`` fixes the rendering, and :func:`croak.gui.sheet.plain` undoes the escape so the (unescaped) HELP keys still resolve to their controls. """ from PySide6.QtWidgets import ( QCheckBox, QFormLayout, - QGroupBox, QLabel, QPushButton, QRadioButton, + QToolButton, ) - from croak.gui.help import _plain + from croak.gui.sheet import Section + from croak.gui.sheet import plain as _plain assert _plain("a && b") == "a & b" @@ -1873,17 +1872,12 @@ def test_widget_labels_escape_literal_ampersands(qtbot): # no mnemonic-parsing label anywhere carries an unescaped ampersand unescaped = [] for page in pages: - for cls in (QCheckBox, QPushButton, QRadioButton): + for cls in (QCheckBox, QPushButton, QRadioButton, QToolButton): unescaped += [ (type(page).__name__, x.text()) for x in page.findChildren(cls) if "&" in x.text().replace("&&", "") ] - unescaped += [ - (type(page).__name__, x.title()) - for x in page.findChildren(QGroupBox) - if "&" in x.title().replace("&&", "") - ] for form in page.findChildren(QFormLayout): for row in range(form.rowCount()): item = form.itemAt(row, QFormLayout.ItemRole.LabelRole) @@ -1897,9 +1891,10 @@ def test_widget_labels_escape_literal_ampersands(qtbot): assert raw_direct.text() == "Skip filtering && regrid (raw → retrieval)" assert _plain(raw_direct.text()) == "Skip filtering & regrid (raw → retrieval)" units = next( - b for b in w.stages[0].findChildren(QGroupBox) if "interaction" in b.title() + b for b in w.stages[0].findChildren(Section) if "interaction" in b.section_title ) - assert _plain(units.title()) == "Units & interaction" + assert units.section_title == "Units & interaction" + assert units.header.text() == "Units && interaction" # ...and their (unescaped) help entries still reach the controls assert "skipping the marginal-check" in raw_direct.toolTip() @@ -2557,31 +2552,16 @@ def _marginal_stage_shown(qtbot, width, height): return w, w.stages[1] -def test_stage_control_column_is_a_collapsible_splitter(qtbot): - """The controls sit on a splitter: 315 px by default, min 240, closable.""" - _w, stage = _marginal_stage_shown(qtbot, 1366, 768) - sp = stage.splitter - assert sp.isCollapsible(0) - assert not sp.isCollapsible(1) - assert sp.sizes()[0] == settings.DEFAULT_CONTROLS_WIDTH - assert sp.widget(0).minimumWidth() == CONTROLS_MIN_WIDTH - before = stage.marg_canvas.width() - sp.setSizes([0, 1]) - qtbot.waitUntil(lambda: sp.sizes()[0] == 0) - qtbot.waitUntil(lambda: stage.marg_canvas.width() >= before + 300) - - -def test_control_column_width_is_shared_and_remembered(qtbot): - """A dragged divider is remembered, reaches the other stages and later runs.""" +def test_stage_plots_move_below_the_form_on_a_narrow_window(qtbot): + """Wide, the form sits beside the plots; narrow, the plots move below it.""" w, stage = _marginal_stage_shown(qtbot, 1366, 768) - sp = stage.splitter - sp.setSizes([260, 1]) - sp.splitterMoved.emit(260, 1) # setSizes() is silent; a real drag emits this - qtbot.waitUntil(lambda: settings.controls_width() == 260) - w.state.stage = 3 # preprocess: shown for the first time, adopts the width - qtbot.waitUntil(lambda: w.stages[2].splitter.sizes()[0] == 260) - again, again_stage = _marginal_stage_shown(qtbot, 1366, 768) - assert again_stage.splitter.sizes()[0] == 260 + assert not stage.sheet.narrow + assert ( + stage.form.geometry().right() + < stage.marg_canvas.mapTo(stage, stage.marg_canvas.rect().topLeft()).x() + ) + w.resize(900, 768) + qtbot.waitUntil(lambda: stage.sheet.narrow) def test_no_stage_needs_more_than_a_small_display(qtbot): From 1743c9a8503440be08c9a750c9681f17fef7fff7 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 22:23:05 +0000 Subject: [PATCH 07/41] Make the workflow window the GUI and retire the wizard croak-gui now opens MainWindow. Data holds the entry's loader and the marginal check as two steps; Prepare and Retrieve are the existing stages; Analyse is a rail with Dispersion and Uncertainty. The primary action drives Continue, Retrieve/Stop and Estimate/Stop, and reopening a session replays it (reusing result.h5 when it was fitted to this trace). The entry pages tell the Data page where to go by a signal instead of calling the wizard. run_wizard becomes run_gui and Wizard becomes MainWindow. The wizard, its welcome page, the state's stage counter and the footer hooks are deleted; the flow tests drive the window through a small numbered-stage driver. Co-Authored-By: Claude Opus 5.5 --- changelog.d/gui-workflow-window.breaking.md | 1 + croak/gui/__init__.py | 40 +- croak/gui/__main__.py | 6 +- croak/gui/base.py | 50 +- croak/gui/branding.py | 4 +- croak/gui/pages.py | 323 ++++++++++ croak/gui/resources/croak.svg | 2 +- croak/gui/settings.py | 2 +- croak/gui/stage.py | 123 ++-- croak/gui/stage_marginal_check.py | 3 +- croak/gui/stage_retrieve.py | 29 +- croak/gui/stage_simulated.py | 35 +- croak/gui/stage_synthetic.py | 18 +- croak/gui/stage_uncertainty.py | 23 +- croak/gui/stage_welcome.py | 73 --- croak/gui/state.py | 33 +- croak/gui/window.py | 116 +++- croak/gui/wizard.py | 603 ------------------ docs/howto/gui.md | 6 +- tests/gui/gui_session.py | 130 ++++ tests/gui/test_base.py | 1 - tests/gui/test_gui_package.py | 28 +- tests/gui/test_stage_marginal_check.py | 2 +- tests/gui/test_stage_simulated.py | 20 +- tests/gui/test_stage_synthetic.py | 22 +- tests/gui/test_state.py | 13 +- tests/gui/test_window.py | 8 + tests/gui/test_window_session.py | 305 +++++++++ tests/gui/test_wizard.py | 244 -------- tests/gui/test_wizard_flow.py | 646 +++++++------------- tests/test_session_files.py | 50 ++ 31 files changed, 1386 insertions(+), 1573 deletions(-) create mode 100644 changelog.d/gui-workflow-window.breaking.md create mode 100644 croak/gui/pages.py delete mode 100644 croak/gui/stage_welcome.py delete mode 100644 croak/gui/wizard.py create mode 100644 tests/gui/gui_session.py create mode 100644 tests/gui/test_window_session.py delete mode 100644 tests/gui/test_wizard.py diff --git a/changelog.d/gui-workflow-window.breaking.md b/changelog.d/gui-workflow-window.breaking.md new file mode 100644 index 0000000..69287e5 --- /dev/null +++ b/changelog.d/gui-workflow-window.breaking.md @@ -0,0 +1 @@ +The GUI is one workflow window instead of a six-step wizard: Home, then Data (load and check the trace), Prepare, Retrieve and Analyse (dispersion and uncertainty), with one primary action in the top bar and the settings or a replay script on request. `croak.gui.run_wizard` is now `croak.gui.run_gui` and `croak.gui.Wizard` is now `croak.gui.MainWindow`; the `croak-gui` command is unchanged. diff --git a/croak/gui/__init__.py b/croak/gui/__init__.py index ee7372d..67945f9 100644 --- a/croak/gui/__init__.py +++ b/croak/gui/__init__.py @@ -1,11 +1,11 @@ -"""PySide6 Qt-Widgets wizard GUI for croak (matplotlib plots). +"""croak's desktop GUI (PySide6, pyqtgraph plots). -The GUI is a thin orchestration layer over the croak library: it loads, cleans, -retrieves, post-processes, tunes dispersion, plots and saves — all via the -library functions and the shared :mod:`croak.plotting` code. +The GUI is a thin layer over the croak library: it loads, cleans, retrieves, +post-processes, tunes dispersion, estimates uncertainty, plots and saves — all +through the library's functions and the Qt-free :mod:`croak.session`. Its window +takes a session from Home through Data, Prepare, Retrieve and Analyse. -Launch with the ``croak-gui`` console script, :func:`run_wizard`, or -``python -m croak.gui``. +Launch with the ``croak-gui`` command, :func:`run_gui`, or ``python -m croak.gui``. """ from __future__ import annotations @@ -17,16 +17,16 @@ # Make the lazy __getattr__ re-exports visible to the type checker without # importing Qt at module import time (the runtime path is __getattr__ below). from .state import WizardState - from .wizard import Wizard + from .window import MainWindow -__all__ = ["main", "run_wizard", "Wizard", "WizardState"] +__all__ = ["MainWindow", "WizardState", "main", "run_gui"] -def run_wizard(argv=None) -> int: - """Create a ``QApplication`` (if needed) and run the wizard event loop. +def run_gui(argv=None) -> int: + """Create a ``QApplication`` (if needed) and run the window's event loop. - The window opens maximised — the plot pages want the room. Un-maximising - gives the size the user last left it (remembered between launches by + The window opens maximised — the plots want the room. Un-maximising gives + the size the user last left it (remembered between launches by :mod:`croak.gui.settings`, clamped to the current screen), or on a first run a default fitted to 90 % of the screen. @@ -43,30 +43,30 @@ def run_wizard(argv=None) -> int: "croak's GUI needs the optional Qt dependencies, which are not " "installed. Install them with `pip install 'croak[gui]'` (or, in " "a uv checkout, `uv sync` — the dev group carries them). " - "Everything except the wizard runs without Qt." + "Everything except the GUI runs without Qt." ) from exc from .branding import APP_NAME, apply_identity - from .wizard import Wizard + from .window import MainWindow args = list(argv if argv is not None else sys.argv) app = QApplication.instance() or QApplication([APP_NAME, *args[1:]]) apply_identity() - window = Wizard() + window = MainWindow() window.showMaximized() return app.exec() def main() -> int: - """Console-script entry point (``croak-gui``); runs the wizard.""" - return run_wizard() + """Console-script entry point (``croak-gui``); runs the GUI.""" + return run_gui() def __getattr__(name): # lazy re-exports (avoid importing Qt at module import) - if name == "Wizard": - from .wizard import Wizard + if name == "MainWindow": + from .window import MainWindow - return Wizard + return MainWindow if name == "WizardState": from .state import WizardState diff --git a/croak/gui/__main__.py b/croak/gui/__main__.py index a232cdb..d85a144 100644 --- a/croak/gui/__main__.py +++ b/croak/gui/__main__.py @@ -1,10 +1,10 @@ -"""Run the wizard with ``python -m croak.gui``.""" +"""Run the GUI with ``python -m croak.gui``.""" from __future__ import annotations import sys -from . import run_wizard +from . import run_gui if __name__ == "__main__": - sys.exit(run_wizard()) + sys.exit(run_gui()) diff --git a/croak/gui/base.py b/croak/gui/base.py index 6a76c5f..cca6fcf 100644 --- a/croak/gui/base.py +++ b/croak/gui/base.py @@ -3,18 +3,19 @@ :class:`Stage` is a :class:`~croak.gui.stage.StagePage` (form of folding sections beside the plots) that keeps the calls the stage modules were written against: ``controls`` to add sections to, :meth:`set_plot_area`, -:meth:`set_status` and the entry-page hooks the old wizard's footer reads. +:meth:`set_status` and the entry pages' :meth:`advance`. """ from __future__ import annotations -from PySide6.QtWidgets import QWidget +from PySide6.QtCore import Signal, SignalInstance +from PySide6.QtWidgets import QPushButton, QWidget from .help import apply_tooltips, flatten from .stage import StagePage from .state import WizardState -__all__ = ["Stage"] +__all__ = ["RunControls", "Stage"] class Stage(StagePage): @@ -23,8 +24,17 @@ class Stage(StagePage): Subclasses add their sections (:func:`croak.gui.canvas.group`) to ``self.controls``, hand their plots to :meth:`set_plot_area`, call :meth:`apply_help` at the end of ``__init__`` and implement :meth:`on_enter`. + + Attributes + ---------- + advanced : Signal(str) + An entry page finished its forward action and says where the session + goes next: ``"check"`` (the marginal check) or ``"retrieve"`` (a raw + simulated scan, straight to Retrieve). """ + advanced = Signal(str) + def __init__(self, state: WizardState, *, find: bool = False): super().__init__(state, find=find) #: Where the sections go: the form's column. @@ -49,15 +59,29 @@ def apply_help(self) -> set[tuple[str, str]]: """ return apply_tooltips(self, flatten(self.HELP)) - # -- entry-page hooks --------------------------------------------------- - # The entry pages (the synthetic generator and the simulated-scan loader) - # are reached from the welcome menu rather than by walking the linear stage - # sequence, so the old wizard cannot decide from ``state.stage`` what its - # footer's "Next" button should say or do. They override these hooks. + def advance(self) -> None: + """Do an entry page's forward action (generate or load), then emit + :attr:`advanced`; the Data page calls it on Continue.""" - def next_label(self) -> str: - """Text for the wizard footer's forward button on this page.""" - return "Next →" - def advance(self) -> None: - """Do this page's forward action (generate/load, then move on).""" +class RunControls: + """Run and Stop buttons that follow a worker, for a page that runs one. + + The page creates :attr:`run_btn` and :attr:`stop_btn`; :meth:`set_running` + toggles them and tells the window (``changed``) so its primary action + becomes Stop while the work runs. Mix in before :class:`Stage`. + """ + + run_btn: QPushButton + stop_btn: QPushButton + changed: SignalInstance + + def set_running(self, running: bool) -> None: + """Show the work as running (Stop on) or idle (Run on).""" + self.run_btn.setEnabled(not running) + self.stop_btn.setEnabled(running) + self.changed.emit() + + def busy(self) -> bool: + """Whether the work is running.""" + return self.stop_btn.isEnabled() diff --git a/croak/gui/branding.py b/croak/gui/branding.py index 25bbcae..e8481ee 100644 --- a/croak/gui/branding.py +++ b/croak/gui/branding.py @@ -2,7 +2,7 @@ Keeps the window-system-facing identity — the name the desktop shows, the icon in the macOS Dock / Windows taskbar / Linux dock — in one place, out of -:func:`~croak.gui.run_wizard`. +:func:`~croak.gui.run_gui`. This module imports Qt, so it must only be imported from *inside* the functions that already need Qt: importing :mod:`croak.gui` deliberately does not pull PySide6 @@ -68,7 +68,7 @@ def apply_identity() -> None: but a ``QApplication`` must already exist. Note the macOS *menu bar* title is not set from here — Qt takes it from - ``argv[0]``, which :func:`~croak.gui.run_wizard` overrides when it constructs + ``argv[0]``, which :func:`~croak.gui.run_gui` overrides when it constructs the ``QApplication``. """ from .. import __version__ diff --git a/croak/gui/pages.py b/croak/gui/pages.py new file mode 100644 index 0000000..e7bceac --- /dev/null +++ b/croak/gui/pages.py @@ -0,0 +1,323 @@ +"""The window's four pages for a session, and the pages made of other pages. + +:func:`default_pages` builds a session's Data, Prepare, Retrieve and Analyse +pages from the shared state. Data is two steps on one page: the entry's own +loader (a measured trace, a simulated scan or a synthetic pulse) and then the +marginal check of the loaded trace. Analyse is a rail of pages that read the +retrieved result. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from typing import cast + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QHBoxLayout, + QPushButton, + QStackedWidget, + QVBoxLayout, +) + +from ..session.pipeline import ( + assemble_load_data, + assemble_simulated_load_data, + assemble_simulated_tracedata, +) +from .base import Stage +from .help import HelpDialog +from .sheet import Segments +from .stage import PageBase +from .stage_dispersion import StageDispersion +from .stage_load import StageLoad, compute_preproc_defaults, trace_signature +from .stage_marginal_check import StageMarginalCheck +from .stage_preprocess import StagePreprocess +from .stage_retrieve import StageRetrieve +from .stage_simulated import StageSimulated, simulated_signature +from .stage_synthetic import StageSynthetic +from .stage_uncertainty import StageUncertainty +from .state import WizardState +from .theme import frame, set_flag +from .workflow import Stage as Place + +__all__ = ["AnalysePage", "DataPage", "RAIL_WIDTH", "default_pages"] + +RAIL_WIDTH = 170 +"""The width of Analyse's rail, logical px.""" + +_ENTRY_STEP = { + "experimental": "Load the trace", + "simulated": "Load the scan", + "synthetic": "Generate the trace", +} + + +class DataPage(PageBase): + """Data: the entry's loader, then the marginal check of what it loaded. + + Parameters + ---------- + state : WizardState + The session; its ``entry`` picks the loader. + adopt_geometry : callable + Retrieve's ``apply_simulated_geometry``, which a simulated scan's loader + calls with the geometry the file records. + """ + + def __init__( + self, state: WizardState, adopt_geometry: Callable[[object], str] + ) -> None: + super().__init__(state) + entry = state.entry + self.entry_page: Stage + if entry == "synthetic": + self.entry_page = StageSynthetic(state) + elif entry == "simulated": + self.entry_page = StageSimulated(state, adopt_geometry) + else: + self.entry_page = StageLoad(state) + self.check = StageMarginalCheck(state) + self._went = "" + head = frame("strip") + hl = QHBoxLayout(head) + hl.setContentsMargins(16, 8, 16, 8) + self.steps = Segments( + [("entry", _ENTRY_STEP[entry]), ("check", "Check the marginal")], + "entry", + ) + self.steps.changed.connect(self.show_step) + hl.addWidget(self.steps) + hl.addStretch(1) + self.stack = QStackedWidget() + self.stack.addWidget(self.entry_page) + self.stack.addWidget(self.check) + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(head) + layout.addWidget(self.stack, stretch=1) + for page in (self.entry_page, self.check): + page.changed.connect(self.changed) + page.message.connect(self.message) + self.entry_page.advanced.connect(self._advanced) + state.load_data_changed.connect(self._sync) + self._sync() + + @property + def step(self) -> str: + """The step on screen: ``"entry"`` or ``"check"``.""" + return self.steps.value + + def current(self) -> Stage: + """The step's page.""" + return self.check if self.step == "check" else self.entry_page + + def _sync(self) -> None: + """Offer the check only once a trace is loaded.""" + self.steps.buttons["check"].setEnabled(self.state.load_data is not None) + self.changed.emit() + + def show_step(self, step: str) -> None: + """Show ``step`` (``"entry"`` or ``"check"``) and refresh it.""" + self.steps.set_value(step) + self.stack.setCurrentWidget(self.current()) + self.current().on_enter() + self.changed.emit() + + def _advanced(self, where: str) -> None: + self._went = where + if where == "check": + self.show_step("check") + + # -- hooks the window reads --------------------------------------------- + def on_enter(self) -> None: + self.current().on_enter() + + def page_key(self) -> str: + return self.step + + def can_advance(self) -> bool: + if self.step == "check" or self.state.entry == "experimental": + return self.state.load_data is not None + return self.entry_page.can_advance() + + def act(self, kind: str) -> bool: + """Continue: from the loader to the check, from the check onwards. + + A measured trace is loaded by its own button, so its Continue only moves + to the check; the synthetic and simulated loaders build the trace here. + A raw simulated scan goes on to Retrieve straight from the loader. + """ + if kind != "continue": + return super().act(kind) + if self.step == "check": + return True + if self.state.entry == "experimental": + self.show_step("check") + return False + self._went = "" + self.entry_page.advance() + return self._went == "retrieve" + + def show_help(self) -> HelpDialog | None: + return self.current().show_help() + + # -- session replay ----------------------------------------------------- + def restore(self) -> bool: + """Reload a reopened session's trace with its saved options. + + Returns whether ``state.load_data`` is set. The saved preprocess settings + are kept (the preprocess defaults are only recorded, for "Reset + defaults"); when nothing can be reloaded the loader says why. + """ + if self.state.entry == "simulated": + return self._restore_simulated() + return self._restore_measured() + + def _restore_measured(self) -> bool: + state = self.state + if not state.load.frog_path: + self.entry_page.set_status( + "Options loaded. Choose a trace file and run the stages.", "warn" + ) + return False + try: + data = assemble_load_data(state.load) + except Exception as exc: # any reader failure is the user's to see + self.entry_page.set_status( + f"Options loaded, but reloading the trace failed: {exc}", "error" + ) + return False + state.set_load_data(data) + state.trace_sig = trace_signature(state.load) + state.preproc_defaults = compute_preproc_defaults(data) + # A measured session's loader is a StageLoad (the entry chose it). + cast(StageLoad, self.entry_page)._draw(data) + return True + + def _restore_simulated(self) -> bool: + state = self.state + # A simulated session's loader is a StageSimulated (the entry chose it). + loader = cast(StageSimulated, self.entry_page) + if not state.simulated.frog_path: + loader.set_status( + "Options loaded. Choose a simulated scan file and run the stages.", + "warn", + ) + return False + loader.seed_widgets(state.simulated) + try: + data = assemble_simulated_load_data(state.simulated) + except Exception as exc: # any reader failure is the user's to see + loader.set_status( + f"Options loaded, but reloading the simulated scan failed: {exc}", + "error", + ) + return False + state.set_load_data(data) + state.trace_sig = simulated_signature(state.simulated) + state.preproc_defaults = compute_preproc_defaults(data) + loader._draw(data) + if state.simulated.raw_direct: + try: + state.set_tracedata(assemble_simulated_tracedata(state.simulated)) + except Exception as exc: # as above + loader.set_status( + f"Options loaded, but the raw simulated trace failed: {exc}", + "error", + ) + return False + return True + + +class AnalysePage(PageBase): + """Analyse: a rail of pages that read the retrieved result. + + Parameters + ---------- + state : WizardState + The session. + pages : sequence of (key, title, page) + The rail's entries, in order; the first is shown first. + """ + + def __init__( + self, state: WizardState, pages: Sequence[tuple[str, str, PageBase]] + ) -> None: + super().__init__(state) + if not pages: + raise ValueError("Analyse needs at least one page") + rail = frame("rail") + rail.setFixedWidth(RAIL_WIDTH) + rl = QVBoxLayout(rail) + rl.setContentsMargins(8, 10, 8, 10) + rl.setSpacing(2) + self.stack = QStackedWidget() + self.pages: dict[str, PageBase] = {} + self.rail_items: dict[str, QPushButton] = {} + for key, title, page in pages: + item = QPushButton(title) + item.setObjectName("railItem") + item.setFocusPolicy(Qt.FocusPolicy.NoFocus) + item.clicked.connect(lambda _=False, key=key: self.show_page(key)) + rl.addWidget(item) + self.rail_items[key] = item + self.pages[key] = page + self.stack.addWidget(page) + page.changed.connect(self.changed) + page.message.connect(self.message) + rl.addStretch(1) + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(rail) + layout.addWidget(self.stack, stretch=1) + self.key = "" + self.show_page(pages[0][0]) + + def show_page(self, key: str) -> None: + """Show the rail's page ``key`` and refresh it.""" + page = self.pages[key] + self.key = key + self.stack.setCurrentWidget(page) + for name, item in self.rail_items.items(): + set_flag(item, "current", name == key) + page.on_enter() + self.changed.emit() + + def on_enter(self) -> None: + self.pages[self.key].on_enter() + + def page_key(self) -> str: + return self.key + + def busy(self) -> bool: + return any(page.busy() for page in self.pages.values()) + + def act(self, kind: str) -> bool: + return self.pages[self.key].act(kind) + + def show_help(self) -> HelpDialog | None: + return self.pages[self.key].show_help() + + +def default_pages(state: WizardState) -> Mapping[Place, PageBase]: + """Build a session's Data, Prepare, Retrieve and Analyse pages.""" + retrieve = StageRetrieve(state) + analyse = AnalysePage( + state, + [ + ("dispersion", "Dispersion", StageDispersion(state)), + ("uncertainty", "Uncertainty", StageUncertainty(state)), + ], + ) + return { + # Looked up at each load, so Retrieve's current method is the one used. + Place.DATA: DataPage( + state, lambda geom: retrieve.apply_simulated_geometry(geom) + ), + Place.PREPARE: StagePreprocess(state), + Place.RETRIEVE: retrieve, + Place.ANALYSE: analyse, + } diff --git a/croak/gui/resources/croak.svg b/croak/gui/resources/croak.svg index a49983f..f6cf1b1 100644 --- a/croak/gui/resources/croak.svg +++ b/croak/gui/resources/croak.svg @@ -2,7 +2,7 @@ croak diff --git a/croak/gui/settings.py b/croak/gui/settings.py index 1d9ad59..64f21c9 100644 --- a/croak/gui/settings.py +++ b/croak/gui/settings.py @@ -7,7 +7,7 @@ by the organisation and application names from :mod:`.branding`; passing those explicitly means the store is the same whether or not :func:`~croak.gui.branding.apply_identity` has run (the tests build a -:class:`~croak.gui.wizard.Wizard` without it). +:class:`~croak.gui.window.MainWindow` without it). Every function opens a short-lived ``QSettings``, reads or writes one value and syncs, so the side effects stay at the edges and the module keeps no state. Only diff --git a/croak/gui/stage.py b/croak/gui/stage.py index ddafebc..044e6ad 100644 --- a/croak/gui/stage.py +++ b/croak/gui/stage.py @@ -1,13 +1,17 @@ -"""The base of a stage page in the workflow window. - -A page is a :class:`~croak.gui.sheet.Sheet`: a form of folding sections on the -left, and on the right an advice box, a progress strip, the derived numbers and -the stage's plots. The window owns the navigation and the one primary action; -a page says what it can do through a few hooks and does it in :meth:`act`. +"""The base of a page in the workflow window. + +The window owns the navigation and the one primary action; a page says what it +can do through a few hooks (:class:`PageBase`) and does it in :meth:`~PageBase.act`. +Most pages are a :class:`StagePage`: a :class:`~croak.gui.sheet.Sheet` with a form +of folding sections on the left and, on the right, an advice box, a progress +strip, the derived numbers and the stage's plots. Pages made of other pages +(Data's steps, Analyse's rail) build on :class:`PageBase` directly. """ from __future__ import annotations +from typing import TYPE_CHECKING + from PySide6.QtCore import Signal from PySide6.QtWidgets import QVBoxLayout, QWidget @@ -15,14 +19,14 @@ from .sheet import AdviceBox, DerivedGrid, Form, ProgressStrip, Sheet from .state import WizardState -__all__ = ["StagePage"] +if TYPE_CHECKING: + from ..plotting import RetrievalPlotData +__all__ = ["PageBase", "StagePage"] -class StagePage(QWidget): - """One stage of a session: its form beside its plots. - Subclasses fill :attr:`form` with sections, add their plots with - :meth:`set_plots`, and override the hooks they need. +class PageBase(QWidget): + """What the window asks of a page. Attributes ---------- @@ -34,52 +38,20 @@ class StagePage(QWidget): """ HELP_TITLE: str = "" - """The stage's name in its help dialog.""" + """The page's name in its help dialog.""" HELP_INTRO: str = "" - """One paragraph on what the stage is for.""" + """One paragraph on what the page is for.""" HELP: HelpSections = [] """Per-setting help, ``[(section, [(label, text), ...]), ...]``.""" changed = Signal() message = Signal(str) - def __init__(self, state: WizardState, *, find: bool = False) -> None: + def __init__(self, state: WizardState) -> None: super().__init__() self.state = state self._help_dialog: HelpDialog | None = None - self.form = Form(find=find) - self.advice = AdviceBox() - self.strip = ProgressStrip() - self.derived = DerivedGrid() - self.derived.hide() # shown once a page adds a number - right = QWidget() - right.setObjectName("page") - self.plot_column = QVBoxLayout(right) - self.plot_column.setContentsMargins(0, 0, 0, 0) - self.plot_column.setSpacing(0) - self.plot_column.addWidget(self.strip) - body = QWidget() - self.body = QVBoxLayout(body) - self.body.setContentsMargins(16, 12, 16, 12) - self.body.setSpacing(10) - self.body.addWidget(self.advice) - self.body.addWidget(self.derived) - self.plot_column.addWidget(body, stretch=1) - self.sheet = Sheet(self.form, right) - outer = QVBoxLayout(self) - outer.setContentsMargins(0, 0, 0, 0) - outer.addWidget(self.sheet) - def add_derived(self, name: str, tooltip: str = "") -> None: - """Add a derived number above the plots (shown as an en dash until set).""" - self.derived.add(name, tooltip) - self.derived.show() - - def set_plots(self, plots: QWidget) -> None: - """Put the stage's plots under its derived numbers; they take the room.""" - self.body.addWidget(plots, stretch=1) - - # -- hooks the window reads --------------------------------------------- def on_enter(self) -> None: """Refresh as the page comes on screen (default: nothing to do).""" @@ -95,10 +67,12 @@ def page_key(self) -> str: """Which sub-page is shown, for pages that have several (default: "").""" return "" - def act(self, kind: str) -> None: - """Do the primary action ``kind`` (see :class:`croak.gui.workflow.Action`). + def act(self, kind: str) -> bool: + """Do the primary action ``kind``; return whether the window moves on. - By default ``"continue"`` has nothing to do before the window moves on. + ``kind`` is a :class:`croak.gui.workflow.Action` kind. By default + ``"continue"`` has nothing to do here and the window moves to the next + stage; a page with steps of its own moves through them first. Raises ------ @@ -106,12 +80,15 @@ def act(self, kind: str) -> None: If this page has no such action. """ if kind == "continue": - return + return True raise ValueError(f"{type(self).__name__} has no {kind!r} action") - # -- help --------------------------------------------------------------- + def overview(self) -> RetrievalPlotData | None: + """The retrieval this page shows, for a saved session's overview PDF.""" + return None + def show_help(self) -> HelpDialog | None: - """Open (or raise) the stage's help dialog; ``None`` if it has no help.""" + """Open (or raise) the page's help dialog; ``None`` if it has no help.""" if not self.HELP: return None if self._help_dialog is None: @@ -121,3 +98,45 @@ def show_help(self) -> HelpDialog | None: self._help_dialog.show() self._help_dialog.raise_() return self._help_dialog + + +class StagePage(PageBase): + """A page that is a sheet: its form beside its plots. + + Subclasses fill :attr:`form` with sections, add their plots with + :meth:`set_plots`, and override the hooks they need. + """ + + def __init__(self, state: WizardState, *, find: bool = False) -> None: + super().__init__(state) + self.form = Form(find=find) + self.advice = AdviceBox() + self.strip = ProgressStrip() + self.derived = DerivedGrid() + self.derived.hide() # shown once a page adds a number + right = QWidget() + right.setObjectName("page") + self.plot_column = QVBoxLayout(right) + self.plot_column.setContentsMargins(0, 0, 0, 0) + self.plot_column.setSpacing(0) + self.plot_column.addWidget(self.strip) + body = QWidget() + self.body = QVBoxLayout(body) + self.body.setContentsMargins(16, 12, 16, 12) + self.body.setSpacing(10) + self.body.addWidget(self.advice) + self.body.addWidget(self.derived) + self.plot_column.addWidget(body, stretch=1) + self.sheet = Sheet(self.form, right) + outer = QVBoxLayout(self) + outer.setContentsMargins(0, 0, 0, 0) + outer.addWidget(self.sheet) + + def add_derived(self, name: str, tooltip: str = "") -> None: + """Add a derived number above the plots (shown as an en dash until set).""" + self.derived.add(name, tooltip) + self.derived.show() + + def set_plots(self, plots: QWidget) -> None: + """Put the stage's plots under its derived numbers; they take the room.""" + self.body.addWidget(plots, stretch=1) diff --git a/croak/gui/stage_marginal_check.py b/croak/gui/stage_marginal_check.py index a1f7b76..89b5e3b 100644 --- a/croak/gui/stage_marginal_check.py +++ b/croak/gui/stage_marginal_check.py @@ -147,9 +147,8 @@ class StageMarginalCheck(Stage): ), ] - def __init__(self, state, wizard): + def __init__(self, state): super().__init__(state) - self.wizard = wizard # Measured default; re-seeded per trace in on_enter (full range for the # noise-free simulated/synthetic traces, 190–1000 nm for measured). self._trust_nm = (190.0, 1000.0) diff --git a/croak/gui/stage_retrieve.py b/croak/gui/stage_retrieve.py index a183e94..c2b24b9 100644 --- a/croak/gui/stage_retrieve.py +++ b/croak/gui/stage_retrieve.py @@ -35,7 +35,7 @@ from ..session.pipeline import extra_param_centres, smearing_kernel from ..truth_metrics import truth_errors from . import settings -from .base import Stage +from .base import RunControls, Stage from .canvas import check, collapsible, combo, dspin, group, spin from .panel_pages import RetrievalPages from .readout import DASH, RetrievalReadout @@ -59,7 +59,7 @@ } -class StageRetrieve(Stage): +class StageRetrieve(RunControls, Stage): HELP_TITLE = "Retrieve" HELP_INTRO = ( "Run the phase-retrieval algorithm on the preprocessed trace. The result " @@ -1460,8 +1460,7 @@ def _run(self): self.set_status( "No compatible previous result to reuse; using the default guess." ) - self.run_btn.setEnabled(False) - self.stop_btn.setEnabled(True) + self.set_running(True) self.save_btn.setEnabled(False) self.retarget_btn.setEnabled(False) self._live_full = False @@ -1497,6 +1496,16 @@ def _stop(self): if self._worker is not None: self._worker.request_stop() + def act(self, kind: str) -> bool: + """Retrieve or stop from the window's primary action (never moves on).""" + if kind == "retrieve": + self._run() + return False + if kind == "stop": + self._stop() + return False + return super().act(kind) + def _on_progress(self, iteration, R, best_R): self._errors.append(R) self.set_status(f"iter {iteration}: R = {R:.4%} (best {best_R:.4%})") @@ -1571,8 +1580,7 @@ def _finalise(self, result, elapsed, *, done_label="Done"): # The worker timed the run itself; prefer its value over the ticking clock. self._elapsed = float(elapsed) self._elapsed_timer.stop() - self.run_btn.setEnabled(True) - self.stop_btn.setEnabled(False) + self.set_running(False) self.save_btn.setEnabled(True) self.retarget_btn.setEnabled(True) self.pf_lam.setEnabled(True) @@ -1589,6 +1597,12 @@ def _on_spectrum_axis(self, axis: str) -> None: settings.set_text("retrieve/spectrum_axis", axis) self._update_view() + def overview(self) -> plotting.RetrievalPlotData | None: + """The current retrieval as the overview's plot data (``None`` before one).""" + if self.state.result is None: + return None + return self._plot_data(self.state.result, self.state.processed) + def _plot_data(self, result, pr) -> plotting.RetrievalPlotData: """Bundle ``result`` and its processed form for the panels. @@ -1863,8 +1877,7 @@ def _update_extras_readout(self, result): ) def _on_failed(self, message): - self.run_btn.setEnabled(True) - self.stop_btn.setEnabled(False) + self.set_running(False) self._elapsed_timer.stop() self._conv_timer.stop() # Keep the last true iteration/error/elapsed; blank the curve table, whose diff --git a/croak/gui/stage_simulated.py b/croak/gui/stage_simulated.py index 7c262eb..96ca7f5 100644 --- a/croak/gui/stage_simulated.py +++ b/croak/gui/stage_simulated.py @@ -182,15 +182,17 @@ class StageSimulated(Stage): ), ] - def __init__(self, state, wizard): + def __init__(self, state, adopt_geometry): super().__init__(state) - self.wizard = wizard + # Retrieve's StageRetrieve.apply_simulated_geometry: adopt the file's + # slab thickness and mask on an interactive load (see advance). + self.adopt_geometry = adopt_geometry self._defaults = SimulatedLoadParams() # the path the file-driven selectors (trace window + thickness) were last # populated for, so re-entering the stage keeps the user's choices self._options_path: str | None = None # Nothing can be loaded until a file previews successfully; drives the - # wizard footer's forward button (see can_advance). + # Data page's Continue (see can_advance). self._can_load = False # debounce rapid control changes into one reload/preview @@ -459,13 +461,13 @@ def _on_band(self, lo, hi): def _on_raw_direct(self, _checked: bool) -> None: # Only changes what Load does (not the preview), so just relabel the - # wizard's forward button (see next_label). - self.wizard.refresh_nav() + # forward action. + self.changed.emit() def _set_can_load(self, can: bool) -> None: """Record whether a trace previewed, and refresh the wizard's Next button.""" self._can_load = can - self.wizard.refresh_nav() + self.changed.emit() def _pick_file(self): fn, _ = QFileDialog.getOpenFileName( @@ -601,13 +603,6 @@ def plot(fig): self.canvas.render(plot) # -- load (the entry-page forward action) ------------------------------- - def next_label(self) -> str: - # "Skip filtering & regrid" changes where Load lands, so say so. The "&&" - # is Qt's escape for a literal ampersand in a button label (a single "&" - # is swallowed as a mnemonic marker, rendering "Load continue →"). - if self.raw_direct.isChecked(): - return "Load raw → Retrieve →" - return "Load && continue →" def can_advance(self) -> bool: return self._can_load @@ -615,11 +610,11 @@ def can_advance(self) -> bool: def advance(self): """Load the chosen simulated scan and hand it to the next stage. - Wired to the wizard footer's forward button. Normally the trace goes into + The Data page's Continue calls it. Normally the trace goes into the marginal-check stage; with "Skip filtering & regrid" ticked the - as-simulated trace is built on its native grid and the wizard jumps - straight to Retrieve. On failure the reason is shown in the status label - and the page stays put. + as-simulated trace is built on its native grid and the session goes + straight to Retrieve (:attr:`advanced` says which). On failure the reason + is shown in the status label and the page stays put. """ params = self._params() try: @@ -642,7 +637,7 @@ def advance(self): # chosen slice, mask hole diameter and spacing). Only on this # interactive path: a restored session keeps its own saved retrieval # settings, which the user may have chosen deliberately. - note = self.wizard.stages[3].apply_simulated_geometry(self.state.geometry) + note = self.adopt_geometry(self.state.geometry) if note: self.set_status(note) if params.raw_direct: @@ -656,6 +651,6 @@ def advance(self): self.set_status(f"Raw load failed: {exc}") return self.state.set_tracedata(td) - self.wizard.enter_retrieve_from_simulated() + self.advanced.emit("retrieve") else: - self.wizard.enter_marginal_from_simulated() + self.advanced.emit("check") diff --git a/croak/gui/stage_synthetic.py b/croak/gui/stage_synthetic.py index d3f9f5a..3084a38 100644 --- a/croak/gui/stage_synthetic.py +++ b/croak/gui/stage_synthetic.py @@ -51,9 +51,8 @@ class StageSynthetic(Stage): """Synthetic-pulse / synthetic-FROG generator (reached from the menu).""" - def __init__(self, state, wizard): + def __init__(self, state): super().__init__(state) - self.wizard = wizard self._last = None # cache of the last computed (grid, ew, omega0) # debounce rapid slider changes into one recompute @@ -172,8 +171,7 @@ def __init__(self, state, wizard): add_wide_row(form, "Poisson counts", self.noise_pois) self.controls.addWidget(box) - # Navigation (Back / "Generate →") lives in the shared wizard footer, via - # the entry-page hooks below. + # Continue lives in the window's top bar; the Data page calls advance. self.controls.addStretch(1) self._seed_sampling() @@ -535,16 +533,14 @@ def _plot_frog(self, ax, lam_sig, delays, trace): ax.set_title("Synthetic FROG trace") # -- generate (the entry-page forward action) --------------------------- - def next_label(self) -> str: - return "Generate →" def advance(self): """Build the synthetic trace from the current settings and move on. - Wired to the wizard footer's forward button. On success the trace (and the - generating field, as the known ground truth) is put on the shared state and - the wizard advances to the marginal-check stage; on failure the reason is - shown in the status label and the page stays put. + The Data page's Continue calls it. On success the trace (and the + generating field, as the known ground truth) is put on the shared state + and :attr:`advanced` sends the session to the marginal check; on failure + the reason is shown in the status label and the page stays put. """ try: grid, ew, omega0 = self._pulse() @@ -582,4 +578,4 @@ def advance(self): self.state.load.interaction = interaction self.state.set_load_data(data) seed_preproc_defaults(self.state) - self.wizard.enter_marginal_from_synthetic() + self.advanced.emit("check") diff --git a/croak/gui/stage_uncertainty.py b/croak/gui/stage_uncertainty.py index f8ee17e..c35bd06 100644 --- a/croak/gui/stage_uncertainty.py +++ b/croak/gui/stage_uncertainty.py @@ -26,7 +26,7 @@ from .. import plotting, save from ..processing import TruthPulse from ..uncertainty import combine_uncertainties -from .base import Stage +from .base import RunControls, Stage from .canvas import MplCanvas, check, combo, dspin, group, spin, with_toolbar _METHODS = ["parametric", "delay", "frequency", "thickness", "covariance"] @@ -39,7 +39,7 @@ def _truth_fwhm(truth: TruthPulse | None) -> float | None: return None if truth is None else truth.fwhm -class StageUncertainty(Stage): +class StageUncertainty(RunControls, Stage): """Estimate and display the statistical uncertainty of the retrieved FWHM.""" HELP_TITLE = "Uncertainty" @@ -322,8 +322,7 @@ def _run(self): # the worker lazily; the worker pulls in the (heavier) solver stack. from .worker import UncertaintyWorker - self.run_btn.setEnabled(False) - self.stop_btn.setEnabled(True) + self.set_running(True) self.set_status("Estimating…") self._worker = UncertaintyWorker( self.state.result, @@ -341,20 +340,28 @@ def _stop(self): if self._worker is not None: self._worker.request_stop() + def act(self, kind: str) -> bool: + """Estimate or stop from the window's primary action (never moves on).""" + if kind == "estimate": + self._run() + return False + if kind == "stop-estimate": + self._stop() + return False + return super().act(kind) + def _on_progress(self, done, total, error): self.set_status(f"replicate {done}/{total}: R = {error:.4%}") def _on_finished(self, uresult): - self.run_btn.setEnabled(True) - self.stop_btn.setEnabled(False) + self.set_running(False) self.state.set_uncertainty_result(uresult) self._refresh_estimates() self._draw(uresult) self.set_status(uresult.summary()) def _on_failed(self, message): - self.run_btn.setEnabled(True) - self.stop_btn.setEnabled(False) + self.set_running(False) if message == "stopped": self.set_status("Stopped.") else: diff --git a/croak/gui/stage_welcome.py b/croak/gui/stage_welcome.py deleted file mode 100644 index 4ffa0eb..0000000 --- a/croak/gui/stage_welcome.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Welcome / main-menu page: the first screen the wizard shows. - -Offers the entry points into the application: load experimental traces for -retrieval (the normal Stage 1 pathway), load a numerically simulated trace -(``scansave`` HDF5 → preprocess), load a previous retrieval (the saved -``options.toml`` replay), or generate a synthetic trace. The page only drives -navigation; the actual work lives in the wizard and the stages it routes to. -""" - -from __future__ import annotations - -from PySide6.QtCore import Qt -from PySide6.QtWidgets import ( - QLabel, - QPushButton, - QSizePolicy, - QVBoxLayout, - QWidget, -) - -__all__ = ["StageWelcome"] - -_WELCOME = ( - "Welcome to croak — a pulse-retrieval workbench.
" - "Choose how you would like to begin." -) - - -class StageWelcome(QWidget): - """A simple centred menu with the entry-point buttons. - - Takes the owning :class:`~croak.gui.wizard.Wizard` so the buttons can drive - navigation (``start_load``/``load_simulated``/``load_previous``/ - ``show_synthetic``). - """ - - def __init__(self, wizard): - super().__init__() - self.wizard = wizard - - outer = QVBoxLayout(self) - outer.addStretch(1) - - title = QLabel("croak") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 42px; font-weight: bold; color: #22324a;") - outer.addWidget(title) - - msg = QLabel(_WELCOME) - msg.setAlignment(Qt.AlignmentFlag.AlignCenter) - msg.setStyleSheet("font-size: 15px; padding: 8px 0 24px 0;") - outer.addWidget(msg) - - for label, slot in ( - ("Load experimental traces for retrieval", wizard.start_load), - ("Load simulated trace", wizard.load_simulated), - ("Load previous retrieval", wizard.load_previous), - ("Generate synthetic trace", wizard.show_synthetic), - ): - btn = QPushButton(label) - btn.setMinimumHeight(56) - btn.setMaximumWidth(420) - btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - btn.setStyleSheet("font-size: 15px; padding: 10px;") - btn.clicked.connect(slot) - wrap = QWidget() - wl = QVBoxLayout(wrap) - wl.setContentsMargins(0, 0, 0, 0) - wl.setAlignment(Qt.AlignmentFlag.AlignCenter) - wl.addWidget(btn) - outer.addWidget(wrap) - - outer.addStretch(2) diff --git a/croak/gui/state.py b/croak/gui/state.py index 30eb121..9fbf1bc 100644 --- a/croak/gui/state.py +++ b/croak/gui/state.py @@ -47,7 +47,6 @@ class WizardState(QObject): """Holds wizard parameters and pipeline outputs, with change signals.""" - stage_changed = Signal(int) load_data_changed = Signal() tracedata_changed = Signal() result_changed = Signal() @@ -55,7 +54,7 @@ class WizardState(QObject): status = Signal(str) # Request the wizard to point the current session at a new dataset folder # (its argument) and replay load → preprocess → retrieve. Emitted by the - # Retrieve stage's "Retarget…" button; handled in croak.gui.wizard.Wizard. + # Retrieve stage's "Retarget…" button; handled in croak.gui.window.MainWindow. retarget_requested = Signal(str) def __init__(self): @@ -75,7 +74,6 @@ def __init__(self): self.dispersion = DispersionParams() self.uncertainty = UncertaintyParams() - self._stage = 1 self.file_datasets: FileDatasets | None = None # FROG file datasets self.load_data: dict | None = None # lam, tau, trace, lam_spec, Ilam_spec self.tracedata: TraceData | None = None @@ -119,34 +117,6 @@ def geometry(self): return self.load_data.get("geometry") return None - # -- navigation --------------------------------------------------------- - @property - def stage(self) -> int: - return self._stage - - @stage.setter - def stage(self, value: int) -> None: - value = max(1, min(6, value)) - if value != self._stage: - self._stage = value - self.stage_changed.emit(value) - - def ready(self, stage: int) -> bool: - """Whether the Next button should be enabled for ``stage``. - - Stage order: Load(1) → Marginal check(2) → Preprocess(3) → Retrieve(4) → - Dispersion(5) → Uncertainty(6). The marginal-check and preprocess stages - only need a loaded trace; the later stages need a completed retrieval. - """ - return { - 1: self.load_data is not None, - 2: self.load_data is not None, - 3: self.load_data is not None, - 4: self.result is not None, - 5: self.result is not None, - 6: True, - }.get(stage, False) - # -- outputs with cascading invalidation -------------------------------- @property def has_data(self) -> bool: @@ -213,7 +183,6 @@ def reset(self) -> None: self.uncertainty_results = {} self.preproc_defaults = None self.trace_sig = None - self._stage = 1 self.load_data_changed.emit() self.tracedata_changed.emit() self.result_changed.emit() diff --git a/croak/gui/window.py b/croak/gui/window.py index d745809..378f9d8 100644 --- a/croak/gui/window.py +++ b/croak/gui/window.py @@ -14,6 +14,7 @@ from collections.abc import Callable, Mapping from pathlib import Path +from typing import TYPE_CHECKING, cast from PySide6.QtCore import Qt from PySide6.QtGui import QAction, QCloseEvent, QKeySequence @@ -36,10 +37,11 @@ from .. import save from ..scripting import generate_script from ..session import files as session_files +from ..session import retarget from . import settings from .branding import app_icon from .home import ENTRY_TILES, Home -from .stage import StagePage +from .stage import PageBase from .state import WizardState from .theme import button, frame, label, load_fonts, set_flag, stylesheet from .workflow import ( @@ -52,9 +54,14 @@ primary_action, ) +if TYPE_CHECKING: + from .pages import DataPage + from .stage_preprocess import StagePreprocess + from .stage_retrieve import StageRetrieve + __all__ = ["DRAWER_WIDTH", "MainWindow", "PageFactory"] -PageFactory = Callable[[WizardState], Mapping[Stage, StagePage]] +PageFactory = Callable[[WizardState], Mapping[Stage, PageBase]] """Builds a session's four stage pages from the shared state.""" DRAWER_WIDTH = 440 @@ -72,21 +79,28 @@ class MainWindow(QMainWindow): Parameters ---------- - make_pages : PageFactory - Builds the Data, Prepare, Retrieve and Analyse pages for a session. + make_pages : PageFactory, optional + Builds the Data, Prepare, Retrieve and Analyse pages for a session + (:func:`croak.gui.pages.default_pages` by default). state : WizardState, optional The shared session state (a fresh one by default). """ - def __init__(self, make_pages: PageFactory, state: WizardState | None = None): + def __init__( + self, make_pages: PageFactory | None = None, state: WizardState | None = None + ): super().__init__() load_fonts() self.setWindowTitle("croak") self.setWindowIcon(app_icon()) settings.restore_window(self) self.state = state or WizardState() + if make_pages is None: + from .pages import default_pages + + make_pages = default_pages self.make_pages = make_pages - self.pages: dict[Stage, StagePage] = {} + self.pages: dict[Stage, PageBase] = {} self.stage = Stage.HOME self.session_path = "" @@ -113,6 +127,7 @@ def __init__(self, make_pages: PageFactory, state: WizardState | None = None): status.hide() status.messageChanged.connect(lambda text: status.setVisible(bool(text))) self.setStyleSheet(stylesheet()) + self.state.retarget_requested.connect(self.retarget) for signal in ( self.state.load_data_changed, self.state.tracedata_changed, @@ -327,8 +342,7 @@ def do_primary(self) -> None: self.save_session() return page = self.pages[self.stage] - page.act(action.kind) - if action.kind == "continue": + if page.act(action.kind) and action.kind == "continue": self.go(next_stage(self.stage, self.progress())) self.refresh() @@ -378,11 +392,92 @@ def open_session(self, path: str) -> bool: self.session_path = path settings.remember_session(path) self._build_pages() - # TODO(gui-redesign): replay the data, preparation and retrieval (or - # rehydrate result.h5) once the new stage pages expose them. self.go(Stage.DATA) + result = Path(path).with_name(session_files.RESULT_FILE) + self.replay(str(result) if result.is_file() else "") return True + def replay(self, result_path: str = "") -> None: + """Bring a reopened session back: its trace, preparation and retrieval. + + The trace is reloaded with the saved options and prepared with the saved + settings. The retrieval is then taken from ``result_path`` (a sibling + ``result.h5``) when it was fitted to exactly this trace + (:func:`croak.session.files.result_mismatch`), and re-run otherwise. A + synthetic session cannot be replayed: its generator's settings are not + part of the saved options. + """ + state = self.state + data = cast("DataPage", self.pages[Stage.DATA]) + if state.entry == "synthetic": + data.entry_page.set_status( + "Options loaded, but the synthetic generator's settings are not " + "saved with a session — regenerate the trace to replay the rest.", + "warn", + ) + return + if not data.restore(): + return + prepare = cast("StagePreprocess", self.pages[Stage.PREPARE]) + if state.tracedata is None: + prepare._seed_all() + prepare.apply(block=True) + if state.tracedata is None: + self.go(Stage.PREPARE) + prepare.set_status( + "Options loaded and data restored; preprocess did not complete.", + "warn", + ) + return + retrieve = cast("StageRetrieve", self.pages[Stage.RETRIEVE]) + # Adopt the saved result before Retrieve is shown: entering it draws + # (and processes) whatever result the session holds. + note = self._rehydrate(result_path) if result_path else "no saved result" + self.go(Stage.RETRIEVE) + if not note: + retrieve.set_status( + "Options and saved retrieval loaded — no re-run needed." + ) + return + if result_path: + retrieve.set_status(f"Options loaded; re-running because {note}.") + retrieve._run() + + def _rehydrate(self, result_path: str) -> str: + """Adopt the saved result at ``result_path``; return why not, or ``""``.""" + tracedata = self.state.tracedata + try: + saved = save.load_result(result_path) + result = save.result_from_saved(saved, grid=tracedata.grid) # pyright: ignore[reportOptionalMemberAccess] + except (KeyError, ValueError, OSError) as exc: + return f"the saved result could not be read ({exc})" + reason = session_files.result_mismatch( + saved, + tracedata, # pyright: ignore[reportArgumentType] + self.state.retrieve, + ) + if not reason: + self.state.set_result(result) + return reason + + def retarget(self, folder: str) -> None: + """Point the session at the dataset in ``folder`` and replay it. + + The dataset-folder paths are rewritten (the shared calibration files + stay) by :func:`croak.session.retarget`; only a measured session has + such a folder, and for the others the reason is shown instead. + """ + try: + moved = retarget(self.state.session_options(), folder) + except ValueError as exc: + self.notify(str(exc)) + return + for field in self.state.load.dataset_path_fields(): + setattr(self.state.load, field, getattr(moved.load, field)) + self._build_pages() + self.go(Stage.DATA) + self.replay() + def save_session(self) -> list[str]: """Ask for a folder and save the session there; return the files written.""" if self.state.result is None: @@ -398,6 +493,7 @@ def save_session(self) -> list[str]: processed=self.state.processed, truth=self.state.truth, uncertainty=self.state.uncertainty_results, + overview=self.pages[Stage.RETRIEVE].overview(), ) self.session_path = str(Path(folder) / session_files.OPTIONS_FILE) settings.remember_session(self.session_path) diff --git a/croak/gui/wizard.py b/croak/gui/wizard.py deleted file mode 100644 index bc6d38e..0000000 --- a/croak/gui/wizard.py +++ /dev/null @@ -1,603 +0,0 @@ -"""The wizard main window. - -A welcome page, a synthetic-trace generator and a simulated-scan loader sit in -front of the six linear stages (Load → Marginal check → Preprocess → Retrieve → -Dispersion → Uncertainty). The welcome page routes to the entry points; the -linear stages and the two entry pages share a header and a footer nav bar (only -the welcome menu hides them), so every forward/backward control sits in the same -bottom-right corner. The synthetic and simulated loaders enter at the -marginal-check stage (their "stage 1"); the experimental Load is stage 1. -""" - -from __future__ import annotations - -import os - -from PySide6.QtGui import QCloseEvent -from PySide6.QtWidgets import ( - QFileDialog, - QHBoxLayout, - QLabel, - QMainWindow, - QMessageBox, - QPushButton, - QStackedWidget, - QVBoxLayout, - QWidget, -) - -from .. import save -from ..session import retarget -from ..session.files import result_mismatch -from . import settings -from .base import Stage -from .branding import app_icon -from .stage_dispersion import StageDispersion -from .stage_load import ( - StageLoad, - assemble_load_data, - compute_preproc_defaults, - trace_signature, -) -from .stage_marginal_check import StageMarginalCheck -from .stage_preprocess import StagePreprocess -from .stage_retrieve import StageRetrieve -from .stage_simulated import ( - StageSimulated, - assemble_simulated_load_data, - assemble_simulated_tracedata, - simulated_signature, -) -from .stage_synthetic import StageSynthetic -from .stage_uncertainty import StageUncertainty -from .stage_welcome import StageWelcome -from .state import WizardState -from .theme import load_fonts, stylesheet - -_TITLES = [ - "1. Load", - "2. Marginal check", - "3. Preprocess", - "4. Retrieve", - "5. Dispersion", - "6. Uncertainty", -] -_NSTAGES = len(_TITLES) -# the six linear stages live behind the welcome (0), synthetic (1) and -# simulated (2) entry pages -_STAGE_OFFSET = 3 - - -class Wizard(QMainWindow): - """Welcome menu + synthetic generator + four-stage pulse-retrieval wizard.""" - - def __init__(self, state: WizardState | None = None): - super().__init__() - self.setWindowTitle("croak — pulse-retrieval wizard") - # run_wizard sets this application-wide, which already covers every window - # it opens; setting it here too means an embedder who built their own - # QApplication still gets the icon. - self.setWindowIcon(app_icon()) - load_fonts() - self.setStyleSheet(stylesheet()) - # Un-maximised size: as the user last left it, else a default clamped to - # the screen (run_wizard maximises on top of this). - settings.restore_window(self) - self.state = state or WizardState() - - central = QWidget() - self.setCentralWidget(central) - layout = QVBoxLayout(central) - - # header bar: stage title (left) + a context-sensitive help button (right) - self._header_bar = QWidget() - self._header_bar.setStyleSheet("background:#22324a;") - hb = QHBoxLayout(self._header_bar) - hb.setContentsMargins(8, 4, 8, 4) - self.header = QLabel() - self.header.setStyleSheet("color:white; font-weight:bold;") - self.help_btn = QPushButton("? Help") - # The header bar is dark (#22324a); give the button an explicit - # high-contrast style so it doesn't disappear into the banner. - self.help_btn.setStyleSheet( - "QPushButton {" - " color:white; font-weight:bold; background:#3a557a;" - " border:1px solid white; border-radius:4px; padding:2px 10px; }" - "QPushButton:hover { background:#4a6a96; }" - ) - self.help_btn.setToolTip("Explain this stage's options") - self.help_btn.clicked.connect(self._show_help) - hb.addWidget(self.header, stretch=1) - hb.addWidget(self.help_btn) - layout.addWidget(self._header_bar) - - # stacked pages: welcome, synthetic, simulated, then the five linear stages - self.stack = QStackedWidget() - self.welcome = StageWelcome(self) - self.synthetic = StageSynthetic(self.state, self) - self.simulated = StageSimulated(self.state, self) - self.stages = [ - StageLoad(self.state), - StageMarginalCheck(self.state, self), - StagePreprocess(self.state), - StageRetrieve(self.state), - StageDispersion(self.state), - StageUncertainty(self.state), - ] - self.stack.addWidget(self.welcome) # index 0 - self.stack.addWidget(self.synthetic) # index 1 - self.stack.addWidget(self.simulated) # index 2 - for s in self.stages: # indices 3..8 - self.stack.addWidget(s) - layout.addWidget(self.stack, stretch=1) - - # footer - self.footer = QWidget() - fl = QHBoxLayout(self.footer) - self.cancel_btn = QPushButton("Cancel") - self.cancel_btn.clicked.connect(self.close) - self.menu_btn = QPushButton("Main menu") - self.menu_btn.clicked.connect(self.leave_to_welcome) - self.back_btn = QPushButton("← Back") - self.back_btn.clicked.connect(self.go_back) - self.next_btn = QPushButton("Next →") - self.next_btn.clicked.connect(self.go_next) - fl.addWidget(self.cancel_btn) - fl.addWidget(self.menu_btn) - fl.addStretch(1) - fl.addWidget(self.back_btn) - fl.addWidget(self.next_btn) - layout.addWidget(self.footer) - - self.state.stage_changed.connect(self._refresh) - for sig in ( - self.state.load_data_changed, - self.state.tracedata_changed, - self.state.result_changed, - ): - sig.connect(self.refresh_nav) - self.state.retarget_requested.connect(self._retarget_session) - - # Which entry the current session came from lives on the state - # (``state.entry``): it selects the live loader params, so it is saved - # with the session and drives Back from preprocess as well. - # which page is on screen: "welcome", "synthetic"/"simulated" (the two - # entry pages) or "stage" (one of the six linear stages). The entry pages - # sit outside the linear sequence, so the footer's Back/Next cannot be - # driven from ``state.stage`` alone. - self._page = "welcome" - self.show_welcome() - - # -- page chrome -------------------------------------------------------- - def _chrome(self, visible: bool) -> None: - self._header_bar.setVisible(visible) - self.footer.setVisible(visible) - - def _entry_page(self) -> Stage | None: - """The entry page currently on screen, or ``None`` on any other page.""" - if self._page == "synthetic": - return self.synthetic - if self._page == "simulated": - return self.simulated - return None - - def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 — Qt override - """Remember the window geometry for the next launch, then close.""" - settings.save_window(self) - super().closeEvent(event) - - def _show_help(self) -> None: - """Open the help dialog for the page currently on screen.""" - page = self._entry_page() or self.stages[self.state.stage - 1] - page.show_help() - - def show_welcome(self) -> None: - self._page = "welcome" - self.stack.setCurrentIndex(0) - self._chrome(False) - - def leave_to_welcome(self) -> None: - """Return to the welcome menu, asking first if the session holds data. - - Every menu entry starts a new session, so a loaded trace or a retrieval - reached this way is discarded the moment the user picks one. Back at - Stage 1 (or on an entry page) and the Main menu button therefore confirm - before leaving; the data itself stays until a menu entry is chosen. - """ - if self.state.has_data and not self._confirm_discard(): - return - self.show_welcome() - - def _confirm_discard(self) -> bool: - """Ask whether to leave the current session behind; True to proceed.""" - box = QMessageBox(self) - box.setIcon(QMessageBox.Icon.Warning) - box.setWindowTitle("Leave this session?") - box.setText("Going back to the main menu leaves the current session behind.") - box.setInformativeText( - "Every menu entry starts a new session, so the loaded trace and any " - "retrieval will be discarded. Save… on the Retrieve stage keeps them." - ) - leave = box.addButton("Back to menu", QMessageBox.ButtonRole.DestructiveRole) - cancel = box.addButton(QMessageBox.StandardButton.Cancel) - box.setDefaultButton(cancel) - box.exec() - return box.clickedButton() is leave - - def show_synthetic(self) -> None: - self._new_session() - self.state.entry = "synthetic" - self._show_synthetic_page() - - def _show_synthetic_page(self) -> None: - """Display the synthetic generator without resetting in-progress data.""" - self._show_entry_page("synthetic", self.synthetic, "Generate synthetic trace") - - def load_simulated(self) -> None: - """Menu entry: load a numerically simulated FROG scan, then preprocess.""" - self._new_session() - self.state.entry = "simulated" - self._show_simulated_page() - - def _show_simulated_page(self) -> None: - """Display the simulated-scan loader without resetting in-progress data.""" - self._show_entry_page("simulated", self.simulated, "Load simulated scan") - - def _show_entry_page(self, page: str, entry: Stage, title: str) -> None: - """Show an entry page with the standard header/footer chrome. - - The entry pages are alternatives to Stage 1 rather than numbered stages, - so the header carries a plain title instead of "Stage N of M"; the footer - gives them the same bottom-right Back/Next as every other page, with Back - returning to the welcome menu and Next running the page's - :meth:`~croak.gui.base.Stage.advance`. - """ - self._page = page - self.stack.setCurrentWidget(entry) - self._chrome(True) - self.header.setText(title) - entry.on_enter() - self.refresh_nav() - - def enter_marginal_from_simulated(self) -> None: - """Show the marginal-check stage for the loaded simulated trace. - - The simulated loader is this session's "stage 1"; it hands the trace to - the marginal-check stage (stage 2), which then leads to preprocessing. - """ - self.goto_stage(2) - - def enter_retrieve_from_simulated(self) -> None: - """Jump straight to Retrieve for a raw, directly-loaded simulated trace. - - The "skip filtering & regrid" path on the simulated loader sets - ``state.tracedata`` itself (the as-simulated trace on its native grid), so - it bypasses the marginal-check (stage 2) and preprocess (stage 3) and - lands on Retrieve (stage 4). ``state.entry`` stays ``"simulated"``, so Back from - Retrieve still reaches Preprocess for a regridded comparison. - """ - self.goto_stage(4) - - def start_load(self) -> None: - """Menu entry 1: begin the experimental-trace workflow at Stage 1.""" - self._new_session() - self.state.entry = "experimental" - self.goto_stage(1) - - def load_previous(self) -> None: - """Menu entry 2: load a previous retrieval (saved ``options.toml``). - - The entry is not chosen here: it comes from the file (``state.entry``), - so a saved simulated session reloads through the simulated loader. - """ - self.state.reset() # _load_options repopulates from the file and rebuilds - self._load_options() - - def _new_session(self) -> None: - """Clear all data/settings and rebuild the stages for a fresh process.""" - self.state.reset() - self._recreate_stages() - - # -- navigation --------------------------------------------------------- - def goto_stage(self, stage: int) -> None: - """Show a linear stage, restoring the header/footer chrome.""" - self._chrome(True) - if self.state.stage != stage: - self.state.stage = stage # emits stage_changed -> _refresh - else: - self._refresh(stage) - - def go_next(self): - # On an entry page "Next" is that page's own forward action (generate the - # synthetic trace / load the simulated scan), which then routes onwards. - entry = self._entry_page() - if entry is not None: - if entry.can_advance(): - entry.advance() - return - if self.state.stage < _NSTAGES and self.state.ready(self.state.stage): - self.state.stage += 1 - - def go_back(self): - # Back from an entry page returns to the welcome menu (it is the session's - # "stage 1", so there is nowhere earlier to go). - if self._entry_page() is not None: - self.leave_to_welcome() - return - # Back from the marginal-check stage (stage 2) returns to whichever entry - # page is this session's "stage 1": the synthetic generator or the - # simulated loader for those entries, else the experimental Load (stage 1). - if self.state.stage == 2 and self.state.entry == "synthetic": - self._show_synthetic_page() - elif self.state.stage == 2 and self.state.entry == "simulated": - self._show_simulated_page() - elif self.state.stage > 1: - self.state.stage -= 1 - else: - self.leave_to_welcome() - - def enter_marginal_from_synthetic(self) -> None: - """Show the marginal-check stage for the generated trace. - - The synthetic generator is this session's "stage 1"; it hands the trace - to the marginal-check stage (stage 2), which then leads to preprocessing. - """ - self.goto_stage(2) - - def _refresh(self, stage: int): - self._page = "stage" - self._chrome(True) - idx = _STAGE_OFFSET + stage - 1 - self.stack.setCurrentIndex(idx) - self.header.setText(f"Stage {stage} of {_NSTAGES} — {_TITLES[stage - 1]}") - self.stages[stage - 1].on_enter() - self.refresh_nav() - - def refresh_nav(self): - """Re-label and re-enable the footer's Back/Next for the current page. - - Public because the entry pages call it when their readiness or forward - action changes (a file previews, "skip filtering & regrid" is toggled). - """ - self.back_btn.setEnabled(True) # Back at Stage 1 returns to the menu - entry = self._entry_page() - if entry is not None: - self.next_btn.setText(entry.next_label()) - self.next_btn.setEnabled(entry.can_advance()) - return - stage = self.state.stage - self.next_btn.setEnabled(stage < _NSTAGES and self.state.ready(stage)) - self.next_btn.setText("Finish" if stage == _NSTAGES else "Next →") - - # -- session persistence ------------------------------------------------ - def _load_options(self): - fn, _ = QFileDialog.getOpenFileName( - self, "Load options.toml", filter="TOML (*.toml)" - ) - if not fn: - return - self.state.from_options(save.load_options(fn)) - # The control widgets are bound one-way (widget -> params), so mutating - # the params alone leaves the UI showing the old values. Rebuild the - # stage pages from the freshly loaded state so every control reflects it. - self._rebuild_stages() - # A sibling result.h5 (written by the Retrieve stage's Save…) lets us - # reload the retrieved result without re-running the solver. - result_path = os.path.join(os.path.dirname(fn), "result.h5") - # Replay the saved settings through the pipeline so the GUI returns to - # (as near as possible) the saved state: same data, options and plots. - self._restore_session(result_path if os.path.exists(result_path) else None) - - def _restore_session(self, result_path: str | None = None): - """Reload the data and re-run preprocess, then retrieve (or reload a result). - - Reproduces the saved session end-to-end: it reloads the trace through the - loader the session was saved with (``state.entry``), without reseeding the - preprocess defaults so the saved windowing/filtering survives, and regrids - with the saved settings. If ``result_path`` points to a saved ``result.h5`` - whose grid matches the regridded trace, the retrieval is **rehydrated from - it** (no solver re-run); otherwise the retrieval is re-run to reproduce the - plots. If no trace file is set (or it can't be reloaded) it stops - gracefully with the controls still populated from the file. - """ - st = self.state - if st.entry == "synthetic": - # The synthetic generator's settings are not part of the saved session - # (they live only in its controls), so there is no trace to rebuild. - self._show_synthetic_page() - self.synthetic.set_status( - "Options loaded, but the synthetic generator's settings are not " - "saved with a session — regenerate the trace to replay the rest." - ) - return - loaded = ( - self._restore_simulated_load() - if st.entry == "simulated" - else self._restore_experimental_load() - ) - if not loaded: - return - # 2) regrid/filter with the saved preprocess settings — unless the loader - # already built the trace on its native grid (the raw simulated path). - if st.tracedata is None: - # stage order: Load(1), Marginal check(2), Preprocess(3), Retrieve(4), … - preproc_stage = self.stages[2] - preproc_stage._seed_all() - preproc_stage.apply(block=True) - if st.tracedata is None: - self.goto_stage(3) - self.set_status( - "Options loaded and data restored; preprocess did not complete." - ) - return - # 3) reproduce the retrieval: rehydrate the saved result if we can (no - # re-run), else re-run the solver to reproduce the plots. - if result_path is not None and self._reload_saved_result(result_path): - self.goto_stage(4) - self.set_status("Options and saved retrieval loaded — no re-run needed.") - return - self.goto_stage(4) - # Say why the saved result was not reused, so a re-run that returns - # slightly different numbers from the saved ones is not a mystery. - note = getattr(self, "_stale_result_note", "") - if result_path is not None and note: - self.set_status(f"Options loaded; re-running because {note}.") - self._stale_result_note = "" - self.stages[3]._run() - - def _restore_experimental_load(self) -> bool: - """Reload the measured trace with the saved dataset/unit selection. - - Returns ``True`` when ``state.load_data`` is set (the caller continues - with preprocess/retrieve); ``False`` after reporting why it stopped. - """ - st = self.state - if not st.load.frog_path: - self.goto_stage(1) - self.set_status("Options loaded. Choose a trace file and run the stages.") - return False - try: - data = assemble_load_data(st.load) - except Exception as exc: - self.goto_stage(1) - self.set_status(f"Options loaded, but reloading the trace failed: {exc}") - return False - # set_load_data (deliberately, unlike StageLoad._load_preview) does NOT - # reseed the preprocess defaults, so the saved grid/window/filter values - # are kept intact. Record the trace signature (and recover the data-seeded - # defaults for the Reset button) so a later reload of the same trace also - # preserves the restored settings. - st.set_load_data(data) - st.trace_sig = trace_signature(st.load) - st.preproc_defaults = compute_preproc_defaults(data) - self.stages[0]._draw(data) - return True - - def _restore_simulated_load(self) -> bool: - """Reload the simulated scan with the saved loader options. - - The simulated counterpart of :meth:`_restore_experimental_load`, mirroring - :meth:`~croak.gui.stage_simulated.StageSimulated.advance` but keeping the - saved preprocess settings. Its "skip filtering & regrid" option builds the - retrieval trace here, on the simulation's native grid, exactly as the - loader page does. Returns ``True`` when the data was restored. - """ - st = self.state - if not st.simulated.frog_path: - self._show_simulated_page() - self.simulated.set_status( - "Options loaded. Choose a simulated scan file and run the stages." - ) - return False - # The loader page's controls are built from the defaults, so reflect the - # restored options in them before anything re-reads them. - self.simulated.seed_widgets(st.simulated) - try: - data = assemble_simulated_load_data(st.simulated) - except Exception as exc: - self._show_simulated_page() - self.simulated.set_status( - f"Options loaded, but reloading the simulated scan failed: {exc}" - ) - return False - st.set_load_data(data) - st.trace_sig = simulated_signature(st.simulated) - st.preproc_defaults = compute_preproc_defaults(data) - self.simulated._draw(data) - if st.simulated.raw_direct: - try: - st.set_tracedata(assemble_simulated_tracedata(st.simulated)) - except Exception as exc: - self._show_simulated_page() - self.simulated.set_status( - f"Options loaded, but the raw simulated trace failed: {exc}" - ) - return False - return True - - def _reload_saved_result(self, result_path: str) -> bool: - """Rehydrate a saved ``result.h5`` onto the regridded trace (no re-run). - - Returns ``True`` when the result was loaded and set on the state (the - Retrieve stage draws it on entry); ``False`` (caller re-runs) when the - file is unreadable or its grid does not match the regridded trace. - - The check is not a formality, and it is deliberately a check on the - *data*, not just on the array shapes. Everything downstream pairs the - saved retrieval with the freshly preprocessed trace — the Retrieve - stage's residual panel divides one by the other — so the saved result is - only safe to adopt if that trace is the one it was actually fitted to. - A shape test alone would not establish this: any preprocessing change - that alters trace values without changing its size (a filtering default, - the edge taper, a recalibration) would slip through and be displayed as - a consistent fit when it is not. See - :func:`croak.session.files.result_mismatch`. - """ - td = self.state.tracedata - if td is None: - return False - try: - saved = save.load_result(result_path) - result = save.result_from_saved(saved, grid=td.grid) - except KeyError, ValueError, OSError: - return False - self._stale_result_note = result_mismatch(saved, td, self.state.retrieve) - if self._stale_result_note: - return False - # processed is recomputed by the Retrieve stage's on_enter/_update_view. - self.state.set_result(result) - return True - - def _retarget_session(self, base_dir: str) -> None: - """Point the current session at a new dataset folder and replay it. - - Triggered by the Retrieve stage's "Retarget…" button. Rewrites the - dataset-folder load paths to ``base_dir`` (keeping the shared calibration - files fixed) via :func:`croak.session.retarget`, refreshes the controls, - then reruns load → preprocess → retrieve with the current settings — so a - working retrieval can be applied to the next, similarly-laid-out dataset. - - Only experimental sessions have such a folder; for the others - :func:`croak.session.retarget` raises and the reason is shown rather than - replaying the session unchanged. - """ - try: - moved = retarget(self.state.session_options(), base_dir) - except ValueError as exc: - self.set_status(str(exc)) - return - # Copy the retargeted paths onto the live load params in place (the stage - # widgets bind to that instance); _recreate_stages then refreshes them. - for field in self.state.load.dataset_path_fields(): - setattr(self.state.load, field, getattr(moved.load, field)) - self._recreate_stages() - self._restore_session() - - def _recreate_stages(self): - """Rebuild the six stage widgets from ``state`` (no navigation). - - The welcome, synthetic and simulated pages (indices 0/1/2) are kept; only - the six linear stages behind them are recreated so their controls rebind - to the current parameter dataclasses. - """ - for s in self.stages: - self.stack.removeWidget(s) - s.deleteLater() - self.stages = [ - StageLoad(self.state), - StageMarginalCheck(self.state, self), - StagePreprocess(self.state), - StageRetrieve(self.state), - StageDispersion(self.state), - StageUncertainty(self.state), - ] - for s in self.stages: - self.stack.addWidget(s) - - def _rebuild_stages(self): - """Recreate the six stage pages and navigate to the current stage.""" - self._recreate_stages() - self.goto_stage(self.state.stage) - - def set_status(self, text: str): - self.stages[self.state.stage - 1].set_status(text) diff --git a/docs/howto/gui.md b/docs/howto/gui.md index 5872319..2cb9121 100644 --- a/docs/howto/gui.md +++ b/docs/howto/gui.md @@ -20,16 +20,16 @@ croak-gui ``` Within a uv project or source checkout, use `uv run croak-gui` or -`uv run python -m croak.gui`. From Python, call {func}`croak.gui.run_wizard`: +`uv run python -m croak.gui`. From Python, call {func}`croak.gui.run_gui`: ```python import croak.gui -croak.gui.run_wizard() +croak.gui.run_gui() ``` {func}`croak.gui.main` is the console-script entry point; -{func}`~croak.gui.run_wizard` creates the `QApplication` and shows the window +{func}`~croak.gui.run_gui` creates the `QApplication` and shows the window **maximised**, because the plot pages need the room. Un-maximising gives the window the size you last left it (remembered between launches and clamped to the screen it opens on), or on a first run a window filling 90 % of the screen. diff --git a/tests/gui/gui_session.py b/tests/gui/gui_session.py new file mode 100644 index 0000000..064ba0b --- /dev/null +++ b/tests/gui/gui_session.py @@ -0,0 +1,130 @@ +"""A test driver for the workflow window, in the old stage numbering. + +The GUI tests were written against six numbered stages (1 Load, 2 Marginal +check, 3 Preprocess, 4 Retrieve, 5 Dispersion, 6 Uncertainty). The window +now has four places (Data with two steps, Prepare, Retrieve, Analyse with a +rail), so :class:`Driver` maps the numbers onto them and hands back the stage +pages, which keep their own behaviour. Navigation tests use the window itself. +""" + +from __future__ import annotations + +from typing import cast + +from croak.gui.pages import AnalysePage, DataPage +from croak.gui.window import MainWindow +from croak.gui.workflow import Stage + +# stage number -> (place, step or rail page) +PLACES = { + 1: (Stage.DATA, "entry"), + 2: (Stage.DATA, "check"), + 3: (Stage.PREPARE, ""), + 4: (Stage.RETRIEVE, ""), + 5: (Stage.ANALYSE, "dispersion"), + 6: (Stage.ANALYSE, "uncertainty"), +} + + +class Driver: + """A :class:`MainWindow` and its stage pages, numbered as the tests know them.""" + + def __init__(self, qtbot, entry: str = "") -> None: + self.win = MainWindow() + qtbot.addWidget(self.win) + # Without an entry: a measured session on its Data page, where the old + # wizard's stage tests began. + self.start(entry or "experimental") + + @classmethod + def of(cls, win: MainWindow) -> Driver: + """A driver for a window the test built and started itself.""" + driver = cls.__new__(cls) + driver.win = win + return driver + + @property + def state(self): + return self.win.state + + def start(self, entry: str) -> None: + """Start a new session from ``entry`` (no confirmation asked).""" + self.win.state.reset() + self.win.state.entry = entry + self.win.session_path = "" + self.win._build_pages() + self.win.go(Stage.DATA) + + # -- pages -------------------------------------------------------------- + @property + def data(self) -> DataPage: + return cast(DataPage, self.win.pages[Stage.DATA]) + + @property + def analyse(self) -> AnalysePage: + return cast(AnalysePage, self.win.pages[Stage.ANALYSE]) + + @property + def stages(self) -> list: + """The six stage pages in the old order (the entry's loader first).""" + return [ + self.data.entry_page, + self.data.check, + self.win.pages[Stage.PREPARE], + self.win.pages[Stage.RETRIEVE], + self.analyse.pages["dispersion"], + self.analyse.pages["uncertainty"], + ] + + @property + def synthetic(self): + return self.data.entry_page + + simulated = synthetic + + # -- where we are --------------------------------------------------------- + def goto(self, number: int) -> None: + """Show stage ``number``, as the old ``state.stage = number`` did. + + The route's rules are bypassed (a test may look at a stage before its + input exists, to check what it says), exactly like the old setter. + """ + place, sub = PLACES[number] + win = self.win + win.stage = place + page = win.pages[place] + win.stack.setCurrentWidget(page) + if place is Stage.DATA: + self.data.show_step(sub) + elif place is Stage.ANALYSE: + self.analyse.show_page(sub) + else: + page.on_enter() + win.refresh() + + @property + def stage_number(self) -> int: + """The old number of the stage on screen (0 on Home).""" + place = self.win.stage + for number, (where, sub) in PLACES.items(): + if where is not place: + continue + if place is Stage.DATA and sub != self.data.step: + continue + if place is Stage.ANALYSE and sub != self.analyse.key: + continue + return number + return 0 + + def current_page(self): + """The stage page on screen (inside Data's steps or Analyse's rail).""" + return self.stages[self.stage_number - 1] if self.stage_number else None + + def __getattr__(self, name: str): + """Anything else is the window's (``resize``, ``grab``, ``statusBar``…).""" + return getattr(self.win, name) + + def rebuild(self) -> None: + """Rebuild the pages from the state (after loading options into it).""" + self.win._build_pages() + self.win.go(Stage.DATA) diff --git a/tests/gui/test_base.py b/tests/gui/test_base.py index a619fb8..370d854 100644 --- a/tests/gui/test_base.py +++ b/tests/gui/test_base.py @@ -37,7 +37,6 @@ def test_stage_help_dialog_is_reused(qtbot): def test_stage_default_hooks(qtbot): stage = Stage(WizardState()) qtbot.addWidget(stage) - assert stage.next_label() == "Next →" assert stage.can_advance() is True assert stage.advance() is None assert stage.on_enter() is None diff --git a/tests/gui/test_gui_package.py b/tests/gui/test_gui_package.py index 2ed4a6a..37fd3a5 100644 --- a/tests/gui/test_gui_package.py +++ b/tests/gui/test_gui_package.py @@ -1,4 +1,4 @@ -"""The ``croak.gui`` package entry points: run_wizard, main, lazy exports, -m.""" +"""The ``croak.gui`` package entry points: run_gui, main, lazy exports, -m.""" from __future__ import annotations @@ -10,46 +10,46 @@ pytest.importorskip("PySide6") import croak.gui as gui -from croak.gui import wizard as wizard_mod +from croak.gui import window as window_mod -def test_run_wizard_shows_maximised_and_runs_the_event_loop(qapp, monkeypatch): +def test_run_gui_shows_maximised_and_runs_the_event_loop(qapp, monkeypatch): shown = [] monkeypatch.setattr( - wizard_mod.Wizard, "showMaximized", lambda self: shown.append(self) + window_mod.MainWindow, "showMaximized", lambda self: shown.append(self) ) monkeypatch.setattr(type(qapp), "exec", lambda self: 42) - assert gui.run_wizard(["croak-gui"]) == 42 + assert gui.run_gui(["croak-gui"]) == 42 assert len(shown) == 1 # apply_identity ran: the application carries croak's name assert qapp.applicationName() == "croak" shown[0].deleteLater() -def test_main_delegates_to_run_wizard(monkeypatch): - monkeypatch.setattr(gui, "run_wizard", lambda argv=None: 7) +def test_main_delegates_to_run_gui(monkeypatch): + monkeypatch.setattr(gui, "run_gui", lambda argv=None: 7) assert gui.main() == 7 -def test_run_wizard_without_qt_says_how_to_install(monkeypatch): +def test_run_gui_without_qt_says_how_to_install(monkeypatch): # A None entry in sys.modules makes the import raise ImportError. monkeypatch.setitem(sys.modules, "PySide6.QtWidgets", None) with pytest.raises(ImportError, match=r"croak\[gui\]"): - gui.run_wizard([]) + gui.run_gui([]) def test_lazy_exports_resolve_and_unknown_names_raise(): from croak.gui.state import WizardState - from croak.gui.wizard import Wizard + from croak.gui.window import MainWindow - assert gui.Wizard is Wizard + assert gui.MainWindow is MainWindow assert gui.WizardState is WizardState with pytest.raises(AttributeError, match="no_such_name"): _ = gui.no_such_name -def test_python_dash_m_runs_the_wizard(monkeypatch): - monkeypatch.setattr(gui, "run_wizard", lambda: 3) +def test_python_dash_m_runs_the_gui(monkeypatch): + monkeypatch.setattr(gui, "run_gui", lambda: 3) # runpy warns when re-running a module that is already imported. monkeypatch.delitem(sys.modules, "croak.gui.__main__", raising=False) with pytest.raises(SystemExit) as exit_info: @@ -61,7 +61,7 @@ def test_importing_the_main_module_does_not_launch(monkeypatch): import importlib launched = [] - monkeypatch.setattr(gui, "run_wizard", lambda: launched.append(1)) + monkeypatch.setattr(gui, "run_gui", lambda: launched.append(1)) monkeypatch.delitem(sys.modules, "croak.gui.__main__", raising=False) importlib.import_module("croak.gui.__main__") assert launched == [] diff --git a/tests/gui/test_stage_marginal_check.py b/tests/gui/test_stage_marginal_check.py index 48f4695..79e0607 100644 --- a/tests/gui/test_stage_marginal_check.py +++ b/tests/gui/test_stage_marginal_check.py @@ -41,7 +41,7 @@ def _data(interaction="pg", *, spectrum=True, trace=None, input_unit="delay"): @pytest.fixture def stage(qtbot): - s = StageMarginalCheck(WizardState(), None) + s = StageMarginalCheck(WizardState()) qtbot.addWidget(s) return s diff --git a/tests/gui/test_stage_simulated.py b/tests/gui/test_stage_simulated.py index d347c30..ba9161c 100644 --- a/tests/gui/test_stage_simulated.py +++ b/tests/gui/test_stage_simulated.py @@ -1,7 +1,8 @@ """Tests for the simulated-scan entry page (:mod:`croak.gui.stage_simulated`). -The page lives inside a :class:`~croak.gui.wizard.Wizard` (it calls back into -it to relabel and advance), so each test drives ``w.simulated``. +The page lives on the window's Data page (Retrieve adopts the scan's geometry +and the Data page follows where it goes), so each test drives ``w.simulated`` +through :class:`gui_session.Driver`. """ from __future__ import annotations @@ -12,20 +13,17 @@ pytest.importorskip("PySide6") +from gui_session import Driver from PySide6.QtWidgets import QFileDialog from croak.gui import stage_simulated -from croak.gui.wizard import Wizard from croak.io import SIMULATED_WINDOW_KEYS from croak.session.params import SimulatedLoadParams @pytest.fixture def wizard(qtbot): - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() - return w + return Driver(qtbot, "simulated") @pytest.fixture @@ -77,11 +75,11 @@ def test_stage_simulated_unreadable_file_falls_back_and_reports(wizard, junk_h5) def test_stage_simulated_advance_reports_load_failure(wizard, junk_h5): sim = wizard.simulated sim.path_edit.setText(junk_h5) - stage_before = wizard.state.stage + stage_before = wizard.stage_number sim.advance() assert sim.status_label.text().startswith("Load failed:") assert wizard.state.load_data is None - assert wizard.state.stage == stage_before + assert wizard.stage_number == stage_before def test_stage_simulated_raw_load_failure_stays_on_the_page( @@ -102,7 +100,7 @@ def broken(params): sim.advance() assert sim.status_label.text() == "Raw load failed: no native grid" assert wizard.state.tracedata is None - assert wizard.stack.currentWidget() is sim + assert wizard.current_page() is sim def test_stage_simulated_advance_without_geometry_note( @@ -116,7 +114,7 @@ def test_stage_simulated_advance_without_geometry_note( sim._populate_file_options(simulated_scan_h5) sim.set_status("before") sim.advance() - assert wizard.stack.currentWidget() is wizard.stages[1] + assert wizard.current_page() is wizard.stages[1] assert sim.status_label.text() == "before" diff --git a/tests/gui/test_stage_synthetic.py b/tests/gui/test_stage_synthetic.py index 83d2d81..0d28ccd 100644 --- a/tests/gui/test_stage_synthetic.py +++ b/tests/gui/test_stage_synthetic.py @@ -14,20 +14,13 @@ from croak.gui.state import WizardState -class _Wizard: - """The one wizard hook the generator calls on success.""" - - def __init__(self): - self.entered = 0 - - def enter_marginal_from_synthetic(self): - self.entered += 1 - - @pytest.fixture def stage(qtbot): - widget = StageSynthetic(WizardState(), _Wizard()) + widget = StageSynthetic(WizardState()) qtbot.addWidget(widget) + # Count where the generator says the session goes on success. + widget.went = [] + widget.advanced.connect(widget.went.append) # Small, fast settings: a coarse trace is plenty to exercise the logic. widget.ndelay.setValue(32) widget.nwl.setValue(64) @@ -142,7 +135,7 @@ def fail(): monkeypatch.setattr(stage, "_pulse", fail) stage.advance() assert stage.status_label.text() == "Generation failed: bad grid" - assert stage.wizard.entered == 0 + assert stage.went == [] def test_generate_with_an_empty_window_asks_to_widen_it(stage): @@ -150,14 +143,13 @@ def test_generate_with_an_empty_window_asks_to_widen_it(stage): stage._wl = (100.0, 150.0) # below every wavelength the grid represents stage.advance() assert "widen the range" in stage.status_label.text() - assert stage.wizard.entered == 0 + assert stage.went == [] assert stage.state.load_data is None def test_generate_hands_the_trace_and_truth_on(stage): - assert stage.next_label() == "Generate →" stage.advance() - assert stage.wizard.entered == 1 + assert stage.went == ["check"] data = stage.state.load_data assert data["trace"].shape == (64, 32) assert data["trace"].max() == pytest.approx(1.0) diff --git a/tests/gui/test_state.py b/tests/gui/test_state.py index 02b7aaf..9e54d81 100644 --- a/tests/gui/test_state.py +++ b/tests/gui/test_state.py @@ -1,4 +1,4 @@ -"""WizardState properties and stage navigation.""" +"""WizardState properties.""" from __future__ import annotations @@ -14,14 +14,3 @@ def test_geometry_and_truth_come_from_the_load(): assert state.geometry is None and state.truth is None state.load_data = {"geometry": "g", "truth": "t"} assert state.geometry == "g" and state.truth == "t" - - -def test_stage_setter_clamps_and_emits_only_on_change(qtbot): - state = WizardState() - seen = [] - state.stage_changed.connect(seen.append) - state.stage = 1 # unchanged: no signal - state.stage = 99 # clamped to the last stage - state.stage = 6 # unchanged again - assert seen == [6] - assert state.stage == 6 diff --git a/tests/gui/test_window.py b/tests/gui/test_window.py index d70d4df..491200b 100644 --- a/tests/gui/test_window.py +++ b/tests/gui/test_window.py @@ -55,6 +55,12 @@ def act(self, kind): self.acted.append(kind) if self.stage is Stage.DATA and kind == "continue": self.state.set_load_data({"trace": "loaded"}) + return kind == "continue" + + def restore(self): + """Data's session replay: the stub has no trace to reload.""" + self.acted.append("restore") + return False def stub_pages(state): @@ -187,6 +193,7 @@ def test_window_raw_simulated_scan_continues_straight_to_retrieve(window): def raw_continue(kind): window.state.set_load_data({"trace": "raw"}) window.state.set_tracedata("native grid") + return True data.act = raw_continue window.do_primary() @@ -280,6 +287,7 @@ def test_window_opens_a_saved_session_and_remembers_it(window, tmp_path, monkeyp assert window.open_session("") assert window.state.entry == "simulated" assert window.stage is Stage.DATA + assert window.pages[Stage.DATA].acted == ["restore"] # replay was asked assert window.context.text() == "Simulated scan · run" assert settings.recent_sessions() == [str(path)] window.go_home() diff --git a/tests/gui/test_window_session.py b/tests/gui/test_window_session.py new file mode 100644 index 0000000..ba3610d --- /dev/null +++ b/tests/gui/test_window_session.py @@ -0,0 +1,305 @@ +"""The window with its real pages: Data's steps, reopening and replaying sessions. + +The end-to-end flows live in ``test_wizard_flow.py``; these target the window's +own branches: Data's two steps, the session replay's early exits, the saved +result's reuse and retargeting. +""" + +from __future__ import annotations + +import numpy as np +import pytest + +pytest.importorskip("PySide6") + +from gui_session import Driver +from PySide6.QtWidgets import QFileDialog, QMessageBox + +from croak import save +from croak.gui import pages, stage_preprocess +from croak.gui.state import WizardState +from croak.gui.window import MainWindow +from croak.gui.workflow import Stage + + +@pytest.fixture +def win(qtbot): + window = MainWindow() + qtbot.addWidget(window) + return window + + +def _point_at_experiment(state, experiment_h5, experiment): + p = state.load + p.frog_path = experiment_h5 + p.ifrog_name, p.lam_name, p.scan_name = "trace", "wavelength", "delay" + p.lam_unit, p.scan_unit, p.scan_type = "nm", "fs", "delay" + p.interaction = experiment.interaction + + +# -- Data's two steps ------------------------------------------------------------ +def test_window_data_waits_for_a_loaded_trace_then_checks_it( + qtbot, experiment_h5, experiment +): + d = Driver(qtbot, "experimental") + win = d.win + assert d.data.step == "entry" + assert not win.primary.isEnabled() # nothing loaded yet + assert not d.data.steps.buttons["check"].isEnabled() + _point_at_experiment(win.state, experiment_h5, experiment) + d.stages[0]._load_preview() + assert win.primary.isEnabled() + assert d.data.steps.buttons["check"].isEnabled() + win.do_primary() # a measured trace: on to the check, still on Data + assert win.stage is Stage.DATA and d.data.step == "check" + assert d.current_page() is d.data.check + win.do_primary() # from the check, on to Prepare + assert win.stage is Stage.PREPARE + assert win.state.tracedata is not None + + +def test_window_data_steps_can_be_chosen_directly(qtbot): + d = Driver(qtbot, "synthetic") + d.synthetic.advance() # generated: the generator moves to the check + assert d.data.step == "check" and d.data.page_key() == "check" + d.data.steps.buttons["entry"].click() + assert d.data.step == "entry" + d.data.on_enter() + d.data.show_help() # the generator has no help of its own: nothing opens + d.data.steps.buttons["check"].click() + dialog = d.data.show_help() + assert dialog is not None + dialog.close() + with pytest.raises(ValueError, match="no 'retrieve' action"): + d.data.act("retrieve") + + +def test_window_synthetic_continue_generates_and_checks(qtbot): + d = Driver(qtbot, "synthetic") + assert d.win.primary.isEnabled() # a synthetic pulse is ready at once + d.win.do_primary() + assert d.state.load_data is not None + assert d.win.stage is Stage.DATA and d.data.step == "check" + + +def test_window_simulated_waits_for_a_previewed_scan(qtbot, simulated_scan_h5): + d = Driver(qtbot, "simulated") + assert not d.win.primary.isEnabled() + d.win.do_primary() # disabled: nothing happens + assert d.data.step == "entry" + d.simulated.path_edit.setText(simulated_scan_h5) + d.simulated._populate_file_options(simulated_scan_h5) + d.simulated._preview() + assert d.win.primary.isEnabled() + d.simulated.raw_direct.setChecked(True) + d.win.do_primary() # raw: straight to Retrieve, Prepare skipped + assert d.win.stage is Stage.RETRIEVE + assert not d.win.stage_buttons[Stage.PREPARE].isEnabled() + + +def test_window_analyse_rail_switches_pages(qtbot): + d = Driver(qtbot, "synthetic") + analyse = d.analyse + analyse.rail_items["uncertainty"].click() + assert analyse.page_key() == "uncertainty" + assert analyse.rail_items["uncertainty"].property("current") is True + assert not analyse.busy() + analyse.on_enter() + dialog = analyse.show_help() + assert dialog is not None + dialog.close() + with pytest.raises(ValueError, match="needs at least one page"): + pages.AnalysePage(WizardState(), []) + + +def test_window_new_session_asks_before_discarding_a_loaded_trace( + win, monkeypatch, experiment_h5, experiment +): + win.new_session("experimental") + _point_at_experiment(win.state, experiment_h5, experiment) + win.pages[Stage.DATA].entry_page._load_preview() + + def answer(label): + def exec_(box): + next(b for b in box.buttons() if b.text() == label).click() + return 0 + + monkeypatch.setattr(QMessageBox, "exec", exec_) + + answer("Cancel") + assert not win.new_session("synthetic") + assert win.state.load_data is not None + answer("Discard") + assert win.new_session("synthetic") + assert win.state.load_data is None and win.state.entry == "synthetic" + + +# -- reopening a session --------------------------------------------------------- +def test_window_open_session_cancelled_leaves_the_session(win, monkeypatch): + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: ("", "")) + win.state.retrieve.maxiters = 7 + assert not win.open_session("") + assert win.state.retrieve.maxiters == 7 + + +def test_window_reopens_a_synthetic_session_without_replaying(win, tmp_path): + src = WizardState() + src.entry = "synthetic" + src.retrieve.maxiters = 77 + opts = tmp_path / "options.toml" + save.save_options(src.to_options(), str(opts)) + (tmp_path / "result.h5").write_bytes(b"") # a sibling result is looked for + assert win.open_session(str(opts)) + assert win.state.retrieve.maxiters == 77 + data = win.pages[Stage.DATA] + assert "synthetic generator's settings are not saved" in ( + data.entry_page.status_label.text() + ) + + +def test_window_replay_needs_a_trace_file(qtbot): + d = Driver(qtbot, "experimental") + d.win.replay() + assert d.stage_number == 1 + assert d.stages[0].status_label.text() == ( + "Options loaded. Choose a trace file and run the stages." + ) + + +def test_window_replay_reports_an_unreadable_trace(qtbot, tmp_path): + d = Driver(qtbot, "experimental") + d.state.load.frog_path = str(tmp_path / "moved.h5") + d.win.replay() + assert d.stage_number == 1 + assert ( + d.stages[0] + .status_label.text() + .startswith("Options loaded, but reloading the trace failed:") + ) + assert d.state.load_data is None + + +def test_window_replay_stops_when_preprocess_fails( + qtbot, monkeypatch, experiment_h5, experiment +): + def broken(params, data, **kwargs): + raise RuntimeError("bad window") + + monkeypatch.setattr(stage_preprocess.pipeline, "build_tracedata", broken) + d = Driver(qtbot, "experimental") + _point_at_experiment(d.state, experiment_h5, experiment) + d.win.replay() + assert d.state.load_data is not None and d.state.tracedata is None + assert d.stage_number == 3 + assert d.stages[2].status_label.text() == ( + "Options loaded and data restored; preprocess did not complete." + ) + + +def test_window_replay_needs_a_simulated_scan_file(qtbot): + d = Driver(qtbot, "simulated") + d.win.replay() + assert d.current_page() is d.simulated + assert d.simulated.status_label.text() == ( + "Options loaded. Choose a simulated scan file and run the stages." + ) + + +def test_window_replay_reports_an_unreadable_scan(qtbot, tmp_path): + d = Driver(qtbot, "simulated") + d.state.simulated.frog_path = str(tmp_path / "moved.h5") + d.win.replay() + assert d.simulated.status_label.text().startswith( + "Options loaded, but reloading the simulated scan failed:" + ) + + +def test_window_replay_reports_a_failed_raw_trace( + qtbot, monkeypatch, simulated_scan_h5 +): + def broken(params): + raise ValueError("no native grid") + + monkeypatch.setattr(pages, "assemble_simulated_tracedata", broken) + d = Driver(qtbot, "simulated") + d.state.simulated.frog_path = simulated_scan_h5 + d.state.simulated.raw_direct = True + d.win.replay() + assert d.simulated.status_label.text() == ( + "Options loaded, but the raw simulated trace failed: no native grid" + ) + assert d.state.tracedata is None + + +def test_window_replay_of_a_raw_simulated_scan_retrieves( + qtbot, monkeypatch, simulated_scan_h5 +): + d = Driver(qtbot, "simulated") + runs = [] + monkeypatch.setattr(d.stages[3], "_run", lambda: runs.append(1)) + d.state.simulated.frog_path = simulated_scan_h5 + d.state.simulated.raw_direct = True + d.win.replay() + assert d.state.tracedata is not None # built on the native grid, no Prepare + assert d.stage_number == 4 and runs == [1] + + +def test_window_rehydrate_refuses_an_unreadable_result(qtbot, tmp_path): + d = Driver(qtbot, "synthetic") + d.synthetic.advance() + d.goto(3) # prepares the trace + reason = d.win._rehydrate(str(tmp_path / "missing.h5")) + assert reason.startswith("the saved result could not be read") + assert d.state.result is None + + +def test_window_retarget_needs_a_measured_session(qtbot, tmp_path): + d = Driver(qtbot, "synthetic") + d.state.retarget_requested.emit(str(tmp_path)) + assert d.win.statusBar().currentMessage() # the reason is shown + assert d.state.entry == "synthetic" + + +def test_window_save_writes_the_overview_too(qtbot, monkeypatch, tmp_path): + d = Driver(qtbot, "synthetic") + d.synthetic.advance() + d.goto(3) + retrieve = d.stages[3] + assert retrieve.overview() is None # nothing retrieved yet + d.state.retrieve.solver = "copra" + d.state.retrieve.maxiters = 3 + retrieve._seed_widgets() + with qtbot.waitSignal(d.state.result_changed, timeout=120_000): + d.goto(4) + retrieve._run() + monkeypatch.setattr( + QFileDialog, "getExistingDirectory", lambda *a, **k: str(tmp_path) + ) + written = d.win.save_session() + assert written == ["result.h5", "options.toml", "retrieval.pdf"] + assert np.isfinite(d.state.result.error) + + +def test_window_primary_actions_reach_retrieve_and_uncertainty(qtbot, monkeypatch): + d = Driver(qtbot, "synthetic") + retrieve, uncertainty = d.stages[3], d.stages[5] + calls = [] + for page, name in ((retrieve, "retrieve"), (uncertainty, "uncertainty")): + monkeypatch.setattr(page, "_run", lambda name=name: calls.append(f"{name} run")) + monkeypatch.setattr( + page, "_stop", lambda name=name: calls.append(f"{name} stop") + ) + # Running work never moves the window on; Continue does, by default. + assert retrieve.act("retrieve") is False + assert retrieve.act("stop") is False + assert retrieve.act("continue") is True + d.analyse.show_page("uncertainty") + assert d.analyse.act("estimate") is False + assert d.analyse.act("stop-estimate") is False + assert d.analyse.act("continue") is True + assert calls == [ + "retrieve run", + "retrieve stop", + "uncertainty run", + "uncertainty stop", + ] diff --git a/tests/gui/test_wizard.py b/tests/gui/test_wizard.py deleted file mode 100644 index 2e30eb6..0000000 --- a/tests/gui/test_wizard.py +++ /dev/null @@ -1,244 +0,0 @@ -"""Tests for the wizard shell (:mod:`croak.gui.wizard`). - -Navigation, the leave-session confirmation, saved-session restore (every -early-exit path) and the saved-result reuse check. The end-to-end flows live -in ``test_wizard_flow.py``; these target the branches those do not reach. -""" - -from __future__ import annotations - -from types import SimpleNamespace - -import numpy as np -import pytest - -pytest.importorskip("PySide6") - -from PySide6.QtWidgets import QFileDialog, QMessageBox - -from croak import save -from croak.gui import stage_preprocess, wizard -from croak.gui.state import WizardState -from croak.gui.wizard import Wizard -from croak.session.files import result_mismatch - - -@pytest.fixture -def w(qtbot): - wiz = Wizard() - qtbot.addWidget(wiz) - return wiz - - -def _point_at_experiment(state, experiment_h5, experiment): - p = state.load - p.frog_path = experiment_h5 - p.ifrog_name, p.lam_name, p.scan_name = "trace", "wavelength", "delay" - p.lam_unit, p.scan_unit, p.scan_type = "nm", "fs", "delay" - p.interaction = experiment.interaction - - -# -- result_mismatch ------------------------------------------------------------ -def _td(trace, omega): - return SimpleNamespace(trace=trace, grid=SimpleNamespace(omega=omega)) - - -@pytest.mark.parametrize( - ("saved", "reason"), - [ - ({}, "the saved result does not store the trace it was fitted to"), - ( - {"trace_meas": np.ones((2, 2))}, - "the saved result is on a 2x2 grid but this trace regrids to 2x3", - ), - ( - {"trace_meas": np.full((2, 3), 2.0)}, - "preprocessing no longer reproduces the trace the result was fitted to", - ), - ( - {"trace_meas": np.ones((2, 3)), "omega": np.arange(3.0)}, - "the saved result is on a different frequency grid", - ), - ( - {"trace_meas": np.ones((2, 3)), "omega": np.array([1.0, 2.5])}, - "the saved result is on a different frequency grid", - ), - ( - { - "trace_meas": np.ones((2, 3)), - "omega": np.array([1.0, 2.0]), - "algorithm": "lm", - }, - "the saved result came from lm but the loaded settings select copra", - ), - ({"trace_meas": np.ones((2, 3)), "omega": np.array([1.0, 2.0])}, ""), - ({"trace_meas": np.ones((2, 3)), "algorithm": "copra"}, ""), - ], -) -def test_wizard_result_mismatch_explains_why_a_result_is_refused(saved, reason): - td = _td(np.ones((2, 3)), np.array([1.0, 2.0])) - assert result_mismatch(saved, td, SimpleNamespace(solver="copra")) == reason - - -def test_wizard_result_mismatch_handles_an_all_zero_trace(): - # a zero peak/span falls back to an absolute tolerance of 1e-9 - td = _td(np.zeros((1, 2)), np.zeros(2)) - saved = {"trace_meas": np.zeros((1, 2)), "omega": np.zeros(2)} - assert result_mismatch(saved, td, SimpleNamespace(solver="copra")) == "" - - -# -- navigation ------------------------------------------------------------------- -def test_wizard_go_next_on_an_entry_page_waits_until_it_can_advance(w): - w.load_simulated() # nothing chosen yet: cannot advance - w.go_next() - assert w.stack.currentWidget() is w.simulated - - -def test_wizard_go_next_advances_a_ready_stage(w, experiment_h5, experiment): - w.start_load() - _point_at_experiment(w.state, experiment_h5, experiment) - w.stages[0]._load_preview() - w.go_next() - assert w.state.stage == 2 - assert w.stack.currentWidget() is w.stages[1] - - -def test_wizard_go_next_stays_on_a_stage_that_is_not_ready(w): - w.start_load() # Stage 1 with no trace loaded - w.go_next() - assert w.state.stage == 1 - - -def _answer_discard(monkeypatch, *, leave): - """Make the leave-session box answer without showing it.""" - - def exec_(box): - label = "Back to menu" if leave else "Cancel" - button = next(b for b in box.buttons() if b.text().replace("&", "") == label) - button.click() - return 0 - - monkeypatch.setattr(QMessageBox, "exec", exec_) - - -def test_wizard_leave_to_welcome_asks_before_discarding_data( - w, monkeypatch, experiment_h5, experiment -): - w.goto_stage(1) - _point_at_experiment(w.state, experiment_h5, experiment) - w.stages[0]._load_preview() - assert w.state.has_data - _answer_discard(monkeypatch, leave=False) - w.leave_to_welcome() - assert w.stack.currentWidget() is w.stages[0] # cancelled: still on Load - _answer_discard(monkeypatch, leave=True) - w.leave_to_welcome() - assert w.stack.currentIndex() == 0 # the welcome menu - assert w.state.load_data is not None # kept until a menu entry is chosen - - -# -- loading a saved session -------------------------------------------------------- -def test_wizard_load_previous_cancelled_keeps_a_fresh_state(w, monkeypatch): - monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: ("", "")) - w.state.retrieve.maxiters = 7 - w.load_previous() - assert w.state.retrieve.maxiters == WizardState().retrieve.maxiters - - -def test_wizard_load_options_restores_a_synthetic_session(w, monkeypatch, tmp_path): - src = WizardState() - src.entry = "synthetic" - src.retrieve.maxiters = 77 - opts = str(tmp_path / "options.toml") - save.save_options(src.to_options(), opts) - (tmp_path / "result.h5").write_bytes(b"") # a sibling result is looked for - monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (opts, "")) - w.load_previous() - assert w.state.retrieve.maxiters == 77 - assert w.stack.currentWidget() is w.synthetic - assert "synthetic generator's settings are not saved" in ( - w.synthetic.status_label.text() - ) - - -def test_wizard_restore_experimental_without_a_trace_file(w): - w.state.entry = "experimental" - w._restore_session() - assert w.state.stage == 1 - assert w.stages[0].status_label.text() == ( - "Options loaded. Choose a trace file and run the stages." - ) - - -def test_wizard_restore_experimental_reports_an_unreadable_trace(w, tmp_path): - w.state.entry = "experimental" - w.state.load.frog_path = str(tmp_path / "moved.h5") - w._restore_session() - assert w.state.stage == 1 - assert ( - w.stages[0] - .status_label.text() - .startswith("Options loaded, but reloading the trace failed:") - ) - assert w.state.load_data is None - - -def test_wizard_restore_stops_when_preprocess_fails( - w, monkeypatch, experiment_h5, experiment -): - def broken(params, data, **kwargs): - raise RuntimeError("bad window") - - monkeypatch.setattr(stage_preprocess.pipeline, "build_tracedata", broken) - w.state.entry = "experimental" - _point_at_experiment(w.state, experiment_h5, experiment) - w._restore_session() - assert w.state.load_data is not None - assert w.state.tracedata is None - assert w.state.stage == 3 - assert w.stages[2].status_label.text() == ( - "Options loaded and data restored; preprocess did not complete." - ) - - -def test_wizard_restore_simulated_without_a_scan_file(w): - w.state.entry = "simulated" - w._restore_session() - assert w.stack.currentWidget() is w.simulated - assert w.simulated.status_label.text() == ( - "Options loaded. Choose a simulated scan file and run the stages." - ) - - -def test_wizard_restore_simulated_reports_an_unreadable_scan(w, tmp_path): - w.state.entry = "simulated" - w.state.simulated.frog_path = str(tmp_path / "moved.h5") - w._restore_session() - assert w.stack.currentWidget() is w.simulated - assert w.simulated.status_label.text().startswith( - "Options loaded, but reloading the simulated scan failed:" - ) - - -def test_wizard_restore_simulated_reports_a_failed_raw_trace( - w, monkeypatch, simulated_scan_h5 -): - def broken(params): - raise ValueError("no native grid") - - monkeypatch.setattr(wizard, "assemble_simulated_tracedata", broken) - w.state.entry = "simulated" - w.state.simulated.frog_path = simulated_scan_h5 - w.state.simulated.raw_direct = True - w._restore_session() - assert w.stack.currentWidget() is w.simulated - assert w.simulated.status_label.text() == ( - "Options loaded, but the raw simulated trace failed: no native grid" - ) - assert w.state.tracedata is None - - -def test_wizard_reload_saved_result_refuses_without_trace_or_file(w, tmp_path): - assert w._reload_saved_result(str(tmp_path / "result.h5")) is False - w.state.tracedata = SimpleNamespace(grid=None) - assert w._reload_saved_result(str(tmp_path / "missing.h5")) is False diff --git a/tests/gui/test_wizard_flow.py b/tests/gui/test_wizard_flow.py index 7b35f96..48ed156 100644 --- a/tests/gui/test_wizard_flow.py +++ b/tests/gui/test_wizard_flow.py @@ -21,6 +21,7 @@ pytest.importorskip("PySide6") +from gui_session import Driver from PySide6.QtCore import QSize, Qt from croak.gui import settings @@ -28,7 +29,7 @@ from croak.gui.stage_load import assemble_load_data, seed_preproc_defaults from croak.gui.stage_marginal_check import StageMarginalCheck from croak.gui.state import WizardState -from croak.gui.wizard import Wizard +from croak.gui.workflow import Stage from croak.uncertainty import UncertaintyResult @@ -58,43 +59,42 @@ def _uresult(method, sd, rng): ) -def test_wizard_opens_on_welcome(qtbot): - w = Wizard() - qtbot.addWidget(w) +def test_window_opens_on_home_then_a_measured_session_on_data(qtbot): + from croak.gui.window import MainWindow + + win = MainWindow() + qtbot.addWidget(win) + assert win.stage is Stage.HOME and win.stack.currentWidget() is win.home + win.home.tiles["experimental"].clicked.emit() + w = Driver.of(win) assert len(w.stages) == 6 - # opens on the welcome page (index 0); the stage header is not yet set - assert w.stack.currentIndex() == 0 - # entering the load workflow shows Stage 1 - w.start_load() - assert w.stack.currentWidget() is w.stages[0] - assert "Stage 1" in w.header.text() - assert not w.next_btn.isEnabled() + assert w.current_page() is w.stages[0] + assert win.context.text() == "Measured trace" + assert not win.primary.isEnabled() # nothing loaded yet def test_synthetic_generation_routes_to_marginal_check(qtbot): """The synthetic generator builds a trace and lands on the marginal-check stage.""" - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() - assert w.stack.currentWidget() is w.synthetic + w = Driver(qtbot) + w.start("synthetic") + assert w.current_page() is w.synthetic # defaults: 10 fs Gaussian at 800 nm, SHG, λ window 350–450 nm w.synthetic.advance() assert w.state.load_data is not None assert w.state.truth is not None - assert w.state.stage == 2 - assert w.stack.currentWidget() is w.stages[1] # StageMarginalCheck + assert w.stage_number == 2 + assert w.current_page() is w.stages[1] # StageMarginalCheck # the trace is regridded only on entering the (next) preprocess stage assert w.state.tracedata is None - w.state.stage = 3 # advance to preprocess + w.goto(3) # advance to preprocess assert w.state.tracedata is not None - assert w.stack.currentWidget() is w.stages[2] # StagePreprocess + assert w.current_page() is w.stages[2] # StagePreprocess def test_synthetic_generates_independent_spectrum_and_steps(qtbot): """Synthetic SHG traces carry a mirrored ×2 fundamental spectrum; steps link.""" - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") s = w.synthetic # step <-> count interlock at fixed range s._on_delay_step(2.0) # 120 fs span / 2 fs -> 61 delays @@ -113,9 +113,8 @@ def test_synthetic_generates_independent_spectrum_and_steps(qtbot): def test_marginal_correction_lives_on_marginal_stage(qtbot): """The SHG marginal correction control is on the marginal stage, not preprocess.""" - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() # SHG synthetic trace with an independent spectrum marginal = w.stages[1] preprocess = w.stages[2] @@ -129,17 +128,16 @@ def test_marginal_correction_lives_on_marginal_stage(qtbot): def test_synthetic_back_returns_to_generator(qtbot): """In a synthetic session, Back from the marginal stage returns to the generator.""" - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() - assert w.state.stage == 2 # marginal check + assert w.stage_number == 2 # marginal check - w.state.stage = 3 # advance to preprocess - w.go_back() # -> marginal check - assert w.stack.currentWidget() is w.stages[1] - w.go_back() # -> the synthetic generator, not the experimental loader - assert w.stack.currentWidget() is w.synthetic + w.goto(3) # advance to preprocess + w.win.stage_buttons[Stage.DATA].click() # back to Data, on its check step + assert w.current_page() is w.stages[1] + w.data.steps.buttons["entry"].click() # -> the synthetic generator + assert w.current_page() is w.synthetic # the in-progress data survives the round trip (no reset on Back) assert w.state.truth is not None @@ -148,10 +146,9 @@ def test_simulated_load_routes_to_marginal_check( qtbot, simulated_scan_h5, simulated_truth ): """Loading a simulated scan lands on the marginal-check stage.""" - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() - assert w.stack.currentWidget() is w.simulated + w = Driver(qtbot) + w.start("simulated") + assert w.current_page() is w.simulated sim = w.simulated sim.path_edit.setText(simulated_scan_h5) sim.band.set_values( @@ -168,12 +165,12 @@ def test_simulated_load_routes_to_marginal_check( # the chosen options are recorded on the state for provenance assert w.state.simulated.interaction == "pg" assert w.state.simulated.frog_path == simulated_scan_h5 - assert w.state.stage == 2 - assert w.stack.currentWidget() is w.stages[1] # StageMarginalCheck + assert w.stage_number == 2 + assert w.current_page() is w.stages[1] # StageMarginalCheck # preprocess regrids on advancing to it - w.state.stage = 3 + w.goto(3) assert w.state.tracedata is not None - assert w.stack.currentWidget() is w.stages[2] # StagePreprocess + assert w.current_page() is w.stages[2] # StagePreprocess def test_simulated_thickness_selector( @@ -182,9 +179,8 @@ def test_simulated_thickness_selector( """The thickness selector lists ``zsave`` slices and disables for legacy files.""" from conftest import SIMULATED_ZSAVE - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated # multi-thickness file: one item per slice, default = the full (exit) thickness @@ -212,9 +208,8 @@ def test_simulated_truth_source_selector( import h5py from conftest import write_simulated_h5 - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated # legacy file stores only the source temporal truth -> disabled, falls to source @@ -237,8 +232,7 @@ def test_simulated_truth_source_selector( def test_dispersive_dz_npoints_coupling(qtbot): """Δz, thickness and Npoints stay tied (npoints = thickness / Δz).""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[3] # StageRetrieve p = stage.state.retrieve @@ -265,8 +259,7 @@ def test_dispersive_dz_npoints_coupling(qtbot): def test_smearing_geometry_sigma_coupling(qtbot): """Hole diameter, spacing and sigma stay tied through the square-BOXCARS map.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[3] # StageRetrieve p = stage.state.retrieve @@ -299,8 +292,7 @@ def test_smearing_geometry_sigma_coupling(qtbot): def test_smearing_gates_the_fit_checkbox_and_solver_support(qtbot): """The group follows solver capability; the fit box follows the enable.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[3] stage.solver_combo.setCurrentText("lbfgs-ad") @@ -321,9 +313,8 @@ def test_simulated_trace_window_lists_iomega_full( qtbot, simulated_scan_multi_h5, simulated_scan_h5 ): """The trace-window dropdown offers ``Iω_full`` only when the file stores it.""" - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated sim.path_edit.setText(simulated_scan_multi_h5) @@ -344,9 +335,8 @@ def test_simulated_band_defaults_to_full_range(qtbot, simulated_scan_multi_h5): """Picking a file seeds the load band to the file's full λ range.""" from croak import io - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated sim._populate_file_options(simulated_scan_multi_h5) lam_lo, lam_hi = io.read_simulated_wavelength_range(simulated_scan_multi_h5) @@ -359,17 +349,17 @@ def test_simulated_raw_direct_routes_to_retrieve(qtbot, simulated_scan_h5): """The raw checkbox loads straight into Retrieve with tracedata + truth set.""" from croak import io - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated sim.path_edit.setText(simulated_scan_h5) sim._populate_file_options(simulated_scan_h5) sim.raw_direct.setChecked(True) - sim.advance() + sim._preview() + w.win.do_primary() # Continue # bypassed marginal-check (2) and preprocess (3): landed on Retrieve (4) - assert w.state.stage == 4 - assert w.stack.currentWidget() is w.stages[3] # StageRetrieve + assert w.stage_number == 4 + assert w.current_page() is w.stages[3] # StageRetrieve assert w.state.tracedata is not None assert w.state.truth is not None # truth still carried for the overlay # native grid: as-simulated (no regrid), so the grid matches the scan's ω axis @@ -379,27 +369,25 @@ def test_simulated_raw_direct_routes_to_retrieve(qtbot, simulated_scan_h5): def test_simulated_back_returns_to_loader(qtbot, simulated_scan_h5, simulated_truth): """In a simulated session, Back from the marginal stage returns to the loader.""" - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") w.simulated.path_edit.setText(simulated_scan_h5) w.simulated.advance() - assert w.state.stage == 2 + assert w.stage_number == 2 - w.state.stage = 3 # advance to preprocess - w.go_back() # -> marginal check - assert w.stack.currentWidget() is w.stages[1] - w.go_back() # -> the simulated loader, not the experimental Stage 1 - assert w.stack.currentWidget() is w.simulated + w.goto(3) # advance to preprocess + w.win.stage_buttons[Stage.DATA].click() # back to Data, on its check step + assert w.current_page() is w.stages[1] + w.data.steps.buttons["entry"].click() # -> the simulated loader + assert w.current_page() is w.simulated def test_marginal_check_predicts_and_auto_fits( qtbot, simulated_scan_h5, simulated_truth ): """The marginal-check stage predicts a marginal and Auto sets a finite exponent.""" - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated sim.path_edit.setText(simulated_scan_h5) sim.band.set_values( @@ -410,7 +398,7 @@ def test_marginal_check_predicts_and_auto_fits( simulated_truth.lam_max / 1e-9 + 10, ) sim.advance() - assert w.state.stage == 2 + assert w.stage_number == 2 mc = w.stages[1] assert isinstance(mc, StageMarginalCheck) data = w.state.load_data @@ -428,30 +416,28 @@ def test_marginal_check_predicts_and_auto_fits( def test_new_session_clears_previous_data(qtbot, experiment_h5, experiment): """Starting a new process from the menu clears prior data and settings.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) # run a synthetic generation, leaving load_data/truth populated - w.show_synthetic() + w.start("synthetic") w.synthetic.advance() assert w.state.truth is not None assert w.state.load_data is not None # switch to the experimental workflow from the menu -> everything cleared - w.start_load() + w.start("experimental") assert w.state.truth is None assert w.state.load_data is None assert w.state.tracedata is None assert w.state.result is None assert w.state.load.frog_path == "" - assert w.stack.currentWidget() is w.stages[0] # StageLoad, fresh + assert w.current_page() is w.stages[0] # StageLoad, fresh def test_solver_combo_lists_all_algorithms(qtbot): from PySide6.QtWidgets import QComboBox - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[3] # StageRetrieve items = { combo.itemText(i) @@ -471,8 +457,7 @@ def test_every_stage_help_entry_maps_to_a_control(qtbot): """ from croak.gui.help import flatten - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) # the simulated loader is an entry page (not in w.stages) but declares HELP too for stage in [*w.stages, w.simulated]: expected = set(flatten(stage.HELP)) @@ -488,10 +473,9 @@ def test_help_dialog_builds_and_shows(qtbot): from croak.gui.help import HelpDialog - w = Wizard() - qtbot.addWidget(w) - w.goto_stage(4) # Retrieve - w._show_help() + w = Driver(qtbot) + w.goto(4) # Retrieve + w.win.show_help() dlg = w.stages[3]._help_dialog assert isinstance(dlg, HelpDialog) assert dlg.isVisible() @@ -517,11 +501,10 @@ def test_load_options_refreshes_controls(qtbot, tmp_path): opts_path = str(tmp_path / "options.toml") save.save_options(src.to_options(), opts_path) - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) # emulate _load_options without the file dialog w.state.from_options(save.load_options(opts_path)) - w._rebuild_stages() + w.rebuild() stage = w.stages[3] # StageRetrieve assert stage.findChildren(QComboBox)[0].currentText() == "lm" @@ -545,8 +528,7 @@ def test_load_options_restores_full_session(qtbot, experiment_h5, experiment, tm from croak import save # Build and save a complete session in one wizard. - w1 = Wizard() - qtbot.addWidget(w1) + w1 = Driver(qtbot) s1 = w1.state s1.load.frog_path = experiment_h5 w1.stages[0]._populate_frog() @@ -554,7 +536,7 @@ def test_load_options_restores_full_session(qtbot, experiment_h5, experiment, tm w1.stages[0]._load_preview() s1.preproc.lam_min_nm = experiment.lam_min / 1e-9 s1.preproc.lam_max_nm = experiment.lam_max / 1e-9 - s1.stage = 3 # preprocess + w1.goto(3) # preprocess assert s1.tracedata is not None s1.retrieve.solver = "copra" s1.retrieve.maxiters = 40 @@ -562,18 +544,17 @@ def test_load_options_restores_full_session(qtbot, experiment_h5, experiment, tm save.save_options(s1.to_options(), opts_path) # Restore into a fresh wizard (emulating _load_options without the dialog). - w2 = Wizard() - qtbot.addWidget(w2) + w2 = Driver(qtbot) w2.state.from_options(save.load_options(opts_path)) - w2._rebuild_stages() + w2.rebuild() with qtbot.waitSignal(w2.state.result_changed, timeout=60000): - w2._restore_session() + w2.win.replay() # Same data, options and (re-computed) result are all restored. assert w2.state.load_data is not None assert w2.state.tracedata is not None assert w2.state.result is not None - assert w2.state.stage == 4 + assert w2.stage_number == 4 assert w2.state.preproc.lam_min_nm == pytest.approx(experiment.lam_min / 1e-9) assert w2.state.load.interaction == experiment.interaction @@ -584,8 +565,7 @@ def test_load_options_restores_defringe_baseline_widgets( """A saved options.toml restores the new de-fringe / baseline controls.""" from croak import save - w1 = Wizard() - qtbot.addWidget(w1) + w1 = Driver(qtbot) s1 = w1.state s1.load.frog_path = experiment_h5 w1.stages[0]._populate_frog() @@ -604,10 +584,9 @@ def test_load_options_restores_defringe_baseline_widgets( save.save_options(s1.to_options(), opts_path) # Restore into a fresh wizard and rebuild the stage widgets from the file. - w2 = Wizard() - qtbot.addWidget(w2) + w2 = Driver(qtbot) w2.state.from_options(save.load_options(opts_path)) - w2._rebuild_stages() + w2.rebuild() p2 = w2.state.preproc assert p2.defringe is True @@ -638,8 +617,7 @@ def test_load_previous_reuses_saved_result_without_rerun( """A sibling result.h5 is rehydrated on load, skipping the solver re-run.""" from croak import save - w1 = Wizard() - qtbot.addWidget(w1) + w1 = Driver(qtbot) s1 = w1.state s1.load.frog_path = experiment_h5 w1.stages[0]._populate_frog() @@ -647,10 +625,10 @@ def test_load_previous_reuses_saved_result_without_rerun( w1.stages[0]._load_preview() s1.preproc.lam_min_nm = experiment.lam_min / 1e-9 s1.preproc.lam_max_nm = experiment.lam_max / 1e-9 - s1.stage = 3 # preprocess + w1.goto(3) # preprocess s1.retrieve.solver = "copra" s1.retrieve.maxiters = 40 - s1.stage = 4 # retrieve + w1.goto(4) # retrieve with qtbot.waitSignal(s1.result_changed, timeout=60000): w1.stages[3]._run() save.save_result( @@ -662,18 +640,17 @@ def test_load_previous_reuses_saved_result_without_rerun( save.save_options(s1.to_options(), str(tmp_path / "options.toml")) # Restore into a fresh wizard; spy on the solver re-run. - w2 = Wizard() - qtbot.addWidget(w2) + w2 = Driver(qtbot) w2.state.from_options(save.load_options(str(tmp_path / "options.toml"))) - w2._rebuild_stages() + w2.rebuild() reran = {"called": False} w2.stages[3]._run = lambda *a, **k: reran.__setitem__("called", True) with qtbot.waitSignal(w2.state.result_changed, timeout=60000): - w2._restore_session(str(tmp_path / "result.h5")) + w2.win.replay(str(tmp_path / "result.h5")) assert w2.state.result is not None # result loaded from disk assert reran["called"] is False # rehydrated, not re-run - assert w2.state.stage == 4 + assert w2.stage_number == 4 # the rehydrated result matches the saved spectrum np.testing.assert_allclose(w2.state.result.spectrum, s1.result.spectrum) @@ -694,8 +671,7 @@ def test_load_previous_reruns_when_saved_result_grid_moved( from croak import save - w1 = Wizard() - qtbot.addWidget(w1) + w1 = Driver(qtbot) s1 = w1.state s1.load.frog_path = experiment_h5 w1.stages[0]._populate_frog() @@ -703,10 +679,10 @@ def test_load_previous_reruns_when_saved_result_grid_moved( w1.stages[0]._load_preview() s1.preproc.lam_min_nm = experiment.lam_min / 1e-9 s1.preproc.lam_max_nm = experiment.lam_max / 1e-9 - s1.stage = 3 + w1.goto(3) s1.retrieve.solver = "copra" s1.retrieve.maxiters = 20 - s1.stage = 4 + w1.goto(4) with qtbot.waitSignal(s1.result_changed, timeout=60000): w1.stages[3]._run() rp = str(tmp_path / "result.h5") @@ -725,13 +701,12 @@ def test_load_previous_reruns_when_saved_result_grid_moved( del f["tau"] f["tau"] = trimmed - w2 = Wizard() - qtbot.addWidget(w2) + w2 = Driver(qtbot) w2.state.from_options(save.load_options(str(tmp_path / "options.toml"))) - w2._rebuild_stages() + w2.rebuild() reran = {"called": False} w2.stages[3]._run = lambda *a, **k: reran.__setitem__("called", True) - w2._restore_session(rp) # must not raise + w2.win.replay(rp) # must not raise assert reran["called"] is True # fell back to a re-run assert "regrids to" in w2.stages[3].status_label.text() # and said why @@ -749,8 +724,7 @@ def test_rehydrated_result_matches_a_rerun_of_the_same_session( """ from croak import save - w1 = Wizard() - qtbot.addWidget(w1) + w1 = Driver(qtbot) s1 = w1.state s1.load.frog_path = experiment_h5 w1.stages[0]._populate_frog() @@ -758,22 +732,21 @@ def test_rehydrated_result_matches_a_rerun_of_the_same_session( w1.stages[0]._load_preview() s1.preproc.lam_min_nm = experiment.lam_min / 1e-9 s1.preproc.lam_max_nm = experiment.lam_max / 1e-9 - s1.stage = 3 + w1.goto(3) s1.retrieve.solver = "copra" s1.retrieve.maxiters = 20 - s1.stage = 4 + w1.goto(4) with qtbot.waitSignal(s1.result_changed, timeout=60000): w1.stages[3]._run() rp = str(tmp_path / "result.h5") save.save_result(s1.result, rp, processed=s1.processed, force=True) save.save_options(s1.to_options(), str(tmp_path / "options.toml")) - w2 = Wizard() - qtbot.addWidget(w2) + w2 = Driver(qtbot) w2.state.from_options(save.load_options(str(tmp_path / "options.toml"))) - w2._rebuild_stages() + w2.rebuild() with qtbot.waitSignal(w2.state.result_changed, timeout=60000): - w2._restore_session(rp) + w2.win.replay(rp) # the trace the reloaded session preprocessed is the one that was fitted np.testing.assert_allclose(w2.state.tracedata.trace, s1.tracedata.trace) @@ -785,7 +758,7 @@ def test_rehydrated_result_matches_a_rerun_of_the_same_session( def _load_simulated_session(w, simulated_scan_h5, simulated_truth): """Drive the simulated loader page to the marginal-check stage.""" - w.load_simulated() + w.start("simulated") sim = w.simulated sim.path_edit.setText(simulated_scan_h5) band = ( @@ -805,8 +778,7 @@ def test_simulated_session_saves_its_trace_provenance( """A simulated session records which scan (and how) it was loaded from.""" from croak import save - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) band = _load_simulated_session(w, simulated_scan_h5, simulated_truth) opts_path = str(tmp_path / "options.toml") @@ -826,17 +798,16 @@ def test_load_previous_restores_a_simulated_session( """A saved simulated session reloads its own scan — not the empty Load stage.""" from croak import save - w1 = Wizard() - qtbot.addWidget(w1) + w1 = Driver(qtbot) _load_simulated_session(w1, simulated_scan_h5, simulated_truth) s1 = w1.state s1.preproc.lam_min_nm = simulated_truth.lam_min / 1e-9 s1.preproc.lam_max_nm = simulated_truth.lam_max / 1e-9 - s1.stage = 3 # preprocess + w1.goto(3) # preprocess assert s1.tracedata is not None s1.retrieve.solver = "copra" s1.retrieve.maxiters = 40 - s1.stage = 4 # retrieve + w1.goto(4) # retrieve with qtbot.waitSignal(s1.result_changed, timeout=60000): w1.stages[3]._run() save.save_result( @@ -845,19 +816,18 @@ def test_load_previous_restores_a_simulated_session( save.save_options(s1.to_options(), str(tmp_path / "options.toml")) # Restore into a fresh wizard (emulating _load_options without the dialog). - w2 = Wizard() - qtbot.addWidget(w2) + w2 = Driver(qtbot) w2.state.from_options(save.load_options(str(tmp_path / "options.toml"))) - w2._rebuild_stages() + w2.rebuild() with qtbot.waitSignal(w2.state.result_changed, timeout=60000): - w2._restore_session(str(tmp_path / "result.h5")) + w2.win.replay(str(tmp_path / "result.h5")) assert w2.state.entry == "simulated" assert w2.state.simulated.frog_path == simulated_scan_h5 assert w2.state.load_data is not None assert w2.state.tracedata is not None assert w2.state.result is not None - assert w2.state.stage == 4 + assert w2.stage_number == 4 # the loader page's controls reflect the restored options, so going Back and # reloading does not silently revert to the defaults assert w2.simulated.path_edit.text() == simulated_scan_h5 @@ -872,23 +842,21 @@ def test_load_previous_restores_a_raw_direct_simulated_session( """The raw simulated path restores its native-grid trace, skipping preprocess.""" from croak import save - w1 = Wizard() - qtbot.addWidget(w1) - w1.load_simulated() + w1 = Driver(qtbot) + w1.start("simulated") w1.simulated.path_edit.setText(simulated_scan_h5) w1.simulated.raw_direct.setChecked(True) w1.simulated._preview() - w1.simulated.advance() - assert w1.state.stage == 4 # straight to Retrieve + w1.win.do_primary() # Continue + assert w1.stage_number == 4 # straight to Retrieve opts_path = str(tmp_path / "options.toml") save.save_options(w1.state.to_options(), opts_path) - w2 = Wizard() - qtbot.addWidget(w2) + w2 = Driver(qtbot) w2.state.from_options(save.load_options(opts_path)) - w2._rebuild_stages() + w2.rebuild() w2.stages[3]._run = lambda *a, **k: None # don't re-run the solver here - w2._restore_session() + w2.win.replay() assert w2.state.simulated.raw_direct is True assert w2.state.tracedata is not None @@ -899,19 +867,15 @@ def test_retarget_reports_that_simulated_sessions_cannot_move( qtbot, simulated_scan_h5, simulated_truth ): """Retarget is experimental-only; a simulated session is told, not replayed.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) _load_simulated_session(w, simulated_scan_h5, simulated_truth) replayed = {"called": False} - w._restore_session = lambda *a, **k: replayed.__setitem__("called", True) + w.win.replay = lambda *a, **k: replayed.__setitem__("called", True) - w._retarget_session("/data/run99") + w.win.retarget("/data/run99") assert replayed["called"] is False - assert ( - "cannot retarget a simulated session" - in w.stages[w.state.stage - 1].status_label.text() - ) + assert "cannot retarget a simulated session" in (w.win.statusBar().currentMessage()) def test_dispersion_round_trips_through_wizard_state(tmp_path): @@ -946,8 +910,7 @@ def test_dispersion_stage_syncs_and_restores_controls(qtbot): import croak from croak.session.params import BeamPathMirror - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[4] # StageDispersion w.state.dispersion.gdd_fs2 = 80.0 w.state.dispersion.material_thickness_mm = {"SiO2": 0.3} @@ -983,8 +946,7 @@ def test_retrieve_stage_retarget_reruns_on_new_dataset(qtbot, tmp_path, experime run01.mkdir() h5 = write_experiment_h5(run01 / "acq.h5", experiment) - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) s = w.state s.load.frog_path = h5 w.stages[0]._populate_frog() @@ -994,7 +956,7 @@ def test_retrieve_stage_retarget_reruns_on_new_dataset(qtbot, tmp_path, experime s.preproc.lam_max_nm = experiment.lam_max / 1e-9 s.retrieve.solver = "copra" s.retrieve.maxiters = 10 - s.stage = 3 # preprocess → tracedata + w.goto(3) # preprocess → tracedata assert s.tracedata is not None # Copy the dataset folder and retarget the session onto it (the handler the @@ -1007,12 +969,11 @@ def test_retrieve_stage_retarget_reruns_on_new_dataset(qtbot, tmp_path, experime assert s.load.frog_path == str(run02 / "acq.h5") assert s.load_data is not None assert s.result is not None # reran load → preprocess → retrieve on the new data - assert s.stage == 4 + assert w.stage_number == 4 def test_load_autodetects_datasets_and_units(qtbot, experiment_h5, experiment): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[0] w.state.load.frog_path = experiment_h5 stage._populate_frog() @@ -1074,8 +1035,7 @@ def test_load_reverse_trace_checkbox_sets_the_param(qtbot, experiment_h5, experi """The Load stage's checkbox writes ``LoadParams.reverse_trace`` live.""" from PySide6.QtWidgets import QCheckBox - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[0] box = next( c for c in stage.findChildren(QCheckBox) if c.text() == "Reverse delay axis" @@ -1095,15 +1055,14 @@ def test_load_reverse_trace_checkbox_sets_the_param(qtbot, experiment_h5, experi def test_preprocess_widgets_show_seeded_wavelengths(qtbot, experiment_h5, experiment): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) state = w.state state.load.frog_path = experiment_h5 w.stages[0]._populate_frog() state.load.interaction = experiment.interaction w.stages[0]._load_preview() lam_nm = state.load_data["lam"] / 1e-9 - state.stage = 3 # preprocess: triggers _seed_ranges -> widgets + w.goto(3) # preprocess: triggers _seed_ranges -> widgets lo, hi = w.stages[2].lam_range.values() # the grid-λ widget must reflect the seeded trace extents, not the 700-900 default assert lo == pytest.approx(np.floor(lam_nm.min()), abs=2) @@ -1115,14 +1074,13 @@ def test_preprocess_interactive_change_recomputes_off_thread( qtbot, experiment_h5, experiment ): """An interactive (debounced) change recomputes via the worker thread.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) state = w.state state.load.frog_path = experiment_h5 w.stages[0]._populate_frog() state.load.interaction = experiment.interaction w.stages[0]._load_preview() - state.stage = 3 # on_enter runs the initial (synchronous) preprocess + w.goto(3) # on_enter runs the initial (synchronous) preprocess stage = w.stages[2] assert state.tracedata is not None td_before = state.tracedata @@ -1142,8 +1100,7 @@ def test_preprocess_interactive_change_recomputes_off_thread( def test_retrieve_solver_combo_greys_regularisation(qtbot): """COPRA (no reg) disables the Regularisation group; LBFGS enables it.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = w.stages[3] rs.solver_combo.setCurrentText("copra") assert not rs.reg_box.isEnabled() @@ -1155,22 +1112,21 @@ def test_retrieve_solver_combo_greys_regularisation(qtbot): def test_retrieve_view_restored_on_reentry(qtbot, experiment_h5, experiment): """Navigating back to preprocess and forward keeps the result and re-shows it.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) st = w.state st.load.frog_path = experiment_h5 w.stages[0]._populate_frog() st.load.interaction = experiment.interaction w.stages[0]._load_preview() - st.stage = 3 # preprocess (synchronous) + w.goto(3) # preprocess (synchronous) rs = w.stages[3] rs.state.retrieve.solver = "copra" rs.state.retrieve.maxiters = 15 - st.stage = 4 # retrieve + w.goto(4) # retrieve with qtbot.waitSignal(st.result_changed, timeout=30000): rs._run() assert st.result is not None - assert w.next_btn.isEnabled() # can proceed to dispersion + assert w.win.primary.isEnabled() # can proceed to dispersion # simulate a fresh stage instance that hasn't drawn the result yet rs._base_result = None @@ -1178,23 +1134,22 @@ def test_retrieve_view_restored_on_reentry(qtbot, experiment_h5, experiment): rs.on_enter() assert rs._base_result is st.result # view restored assert rs.save_btn.isEnabled() - assert w.next_btn.isEnabled() + assert w.win.primary.isEnabled() def test_retrieve_reuse_previous_result_guess(qtbot, experiment_h5, experiment): """'Reuse previous result' seeds the next run from the prior spectrum.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) st = w.state st.load.frog_path = experiment_h5 w.stages[0]._populate_frog() st.load.interaction = experiment.interaction w.stages[0]._load_preview() - st.stage = 3 + w.goto(3) rs = w.stages[3] rs.state.retrieve.solver = "copra" rs.state.retrieve.maxiters = 15 - st.stage = 4 + w.goto(4) with qtbot.waitSignal(st.result_changed, timeout=30000): rs._run() first = st.result.spectrum.copy() @@ -1210,8 +1165,7 @@ def test_retrieve_reuse_previous_result_guess(qtbot, experiment_h5, experiment): def test_retrieve_initial_guess_selector_is_mutually_exclusive(qtbot): """Selecting one GUI seed clears every previously selected seed flag.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = w.stages[3] p = rs.state.retrieve @@ -1232,8 +1186,7 @@ def test_retrieve_reuse_passes_the_previous_fitted_extras(qtbot, monkeypatch): """'Reuse previous result' hands the last result on as the warm-start seed.""" from croak.gui import stage_retrieve as sr - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) st = w.state rs = w.stages[3] @@ -1293,8 +1246,7 @@ def test_retrieve_status_reports_the_fitted_smearing_width(qtbot): """A fitted smearing strength is reported as a delay width and its factor.""" from croak.session.pipeline import smearing_kernel - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = w.stages[3] p = rs.state.retrieve p.smearing = True @@ -1332,8 +1284,7 @@ def test_position_scan_seeds_delay(experiment_h5, experiment): def test_full_flow(qtbot, experiment_h5, experiment): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) state = w.state # Stage 1: load via the real flow @@ -1342,18 +1293,18 @@ def test_full_flow(qtbot, experiment_h5, experiment): state.load.interaction = experiment.interaction w.stages[0]._load_preview() assert state.load_data is not None - assert w.next_btn.isEnabled() + assert w.win.primary.isEnabled() # narrow the retrieval grid to the useful band (keeps the test fast) state.preproc.lam_min_nm = experiment.lam_min / 1e-9 state.preproc.lam_max_nm = experiment.lam_max / 1e-9 # Stage 3: preprocess - state.stage = 3 + w.goto(3) assert state.tracedata is not None # Stage 4: retrieve (threaded) - state.stage = 4 + w.goto(4) state.retrieve.solver = "copra" state.retrieve.maxiters = 40 with qtbot.waitSignal(state.result_changed, timeout=60000): @@ -1362,7 +1313,7 @@ def test_full_flow(qtbot, experiment_h5, experiment): assert state.result.error < 0.2 # Stage 5: dispersion tuning - state.stage = 5 + w.goto(5) disp = w.stages[4] disp.gdd.set_value(10.0) disp._changed() @@ -1391,8 +1342,7 @@ def test_dispersion_gas_updates_pulse(qtbot): import croak from croak.maths import wlfreq - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) g = croak.Grid(48, dt=0.6e-15) ew = croak.gaussian_pulse(g, 7e-15) delays = np.linspace(-30e-15, 30e-15, 30) @@ -1440,8 +1390,7 @@ def test_dispersion_mirror_stack_removes_beam_path_mirrors(qtbot): from croak import dispersion from croak.maths import wlfreq - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) g = croak.Grid(48, dt=0.6e-15) ew = croak.gaussian_pulse(g, 7e-15) delays = np.linspace(-30e-15, 30e-15, 30) @@ -1521,8 +1470,7 @@ def test_sci_spinbox_decade_aware_stepping(qtbot): def test_reload_same_trace_keeps_preproc(qtbot, experiment_h5, experiment): """Reloading the *same* trace must not discard the user's preprocess windows.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) state = w.state state.load.frog_path = experiment_h5 w.stages[0]._populate_frog() @@ -1547,8 +1495,7 @@ def test_reload_same_trace_keeps_preproc(qtbot, experiment_h5, experiment): def test_preprocess_defringe_baseline_controls(qtbot, experiment_h5, experiment): """The new de-fringe / baseline controls drive params, gate, and round-trip.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) state = w.state state.load.frog_path = experiment_h5 w.stages[0]._populate_frog() @@ -1606,8 +1553,7 @@ def test_preprocess_defringe_baseline_controls(qtbot, experiment_h5, experiment) def test_preprocess_reset_button_restores_defaults(qtbot, experiment_h5, experiment): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) state = w.state state.load.frog_path = experiment_h5 w.stages[0]._populate_frog() @@ -1615,7 +1561,7 @@ def test_preprocess_reset_button_restores_defaults(qtbot, experiment_h5, experim w.stages[0]._load_preview() default_lam = state.preproc_defaults.lam_min_nm - state.stage = 3 + w.goto(3) pre = w.stages[2] state.preproc.lam_min_nm = 321.0 state.preproc.filter_dc = True @@ -1627,8 +1573,7 @@ def test_preprocess_reset_button_restores_defaults(qtbot, experiment_h5, experim def test_retrieve_reset_button_restores_defaults(qtbot, experiment_h5, experiment): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) ret = w.stages[3] state = w.state state.retrieve.solver = "lm" @@ -1649,8 +1594,7 @@ def test_retrieve_reset_button_restores_defaults(qtbot, experiment_h5, experimen # --------------------------------------------------------------------------- def test_stage6_combine_estimates(qtbot): """Ticking two retained estimates and combining produces a 'combined' result.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[5] rng = np.random.default_rng(0) w.state.uncertainty_results = { @@ -1676,8 +1620,7 @@ def test_stage6_save_appends_uncertainty(qtbot, tmp_path, monkeypatch): import croak from croak.maths import wlfreq - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[5] g = croak.Grid(48, dt=0.6e-15) ew = croak.gaussian_pulse(g, 7e-15) @@ -1718,11 +1661,10 @@ def test_preprocess_reseeds_every_control_on_entry(qtbot): a different set of options was actually applied — the user had to un-tick and re-tick each one to force it through. """ - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() - w.state.stage = 3 # preprocess + w.goto(3) # preprocess stage = w.stages[2] # mutate the parameters behind the widgets' backs, as a re-load would @@ -1751,9 +1693,8 @@ def test_simulated_reload_keeps_preproc_settings(qtbot, simulated_scan_h5): Same rule as the experimental loader: only a genuinely different trace (here a narrower loaded λ band, which moves the data-seeded defaults) resets them. """ - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated sim.path_edit.setText(simulated_scan_h5) sim._populate_file_options(simulated_scan_h5) @@ -1764,14 +1705,14 @@ def test_simulated_reload_keeps_preproc_settings(qtbot, simulated_scan_h5): w.state.preproc.use_threshold = True # Back to the loader and forward again with the same options -> settings kept - w.go_back() # stage 2 -> the simulated loader (this session's "stage 1") - assert w.stack.currentWidget() is w.simulated + w.data.steps.buttons["entry"].click() # back to the simulated loader + assert w.current_page() is w.simulated sim.advance() assert w.state.preproc.defringe is True assert w.state.preproc.use_threshold is True # a different loaded band is a different trace -> data-seeded defaults return - w.go_back() + w.data.steps.buttons["entry"].click() lo, hi = sim.band.values() sim.band.set_values(lo + 5.0, hi - 5.0) sim._band = (lo + 5.0, hi - 5.0) @@ -1780,44 +1721,6 @@ def test_simulated_reload_keeps_preproc_settings(qtbot, simulated_scan_h5): assert w.state.preproc.use_threshold is False -def test_entry_pages_use_the_wizard_footer(qtbot, simulated_scan_h5): - """Both entry pages navigate through the shared bottom-right Back/Next.""" - w = Wizard() - qtbot.addWidget(w) - - # -- synthetic generator -- - w.show_synthetic() - assert w.footer.isVisible() or not w.isVisible() # chrome shown, not hidden - assert w._header_bar.isVisibleTo(w) - assert w.next_btn.text() == "Generate →" - assert w.next_btn.isEnabled() - w.go_back() - assert w.stack.currentWidget() is w.welcome - w.show_synthetic() - w.go_next() # Next == Generate - assert w.state.stage == 2 - assert w.stack.currentWidget() is w.stages[1] - - # -- simulated loader -- - w.load_simulated() - sim = w.simulated - assert not w.next_btn.isEnabled() # nothing previewed yet - # "&&" is Qt's escape for a literal "&" in a button label - assert w.next_btn.text() == "Load && continue →" - sim.path_edit.setText(simulated_scan_h5) - sim._populate_file_options(simulated_scan_h5) - sim._preview() - assert w.next_btn.isEnabled() - # the raw path changes where Next lands, so it relabels - sim.raw_direct.setChecked(True) - assert w.next_btn.text() == "Load raw → Retrieve →" - sim.raw_direct.setChecked(False) - # "&&" is Qt's escape for a literal "&" in a button label - assert w.next_btn.text() == "Load && continue →" - w.go_back() - assert w.stack.currentWidget() is w.welcome - - def test_slider_controls_fill_the_control_column(qtbot): """Range sliders span their group box rather than collapsing to the spins. @@ -1825,11 +1728,10 @@ def test_slider_controls_fill_the_control_column(qtbot): (``FieldsStayAtSizeHint`` on macOS), which ignored the controls' ``Expanding`` size policy and left the sliders as narrow as the two spin boxes. """ - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() - w.state.stage = 3 + w.goto(3) stage = w.stages[2] w.resize(1500, 950) w.show() @@ -1865,9 +1767,9 @@ def test_widget_labels_escape_literal_ampersands(qtbot): assert _plain("a && b") == "a & b" - w = Wizard() - qtbot.addWidget(w) - pages = [w.welcome, w.synthetic, w.simulated, *w.stages] + w = Driver(qtbot) + simulated = Driver(qtbot, "simulated") + pages = [w.win.home, *w.stages, simulated.simulated] # no mnemonic-parsing label anywhere carries an unescaped ampersand unescaped = [] @@ -1887,7 +1789,7 @@ def test_widget_labels_escape_literal_ampersands(qtbot): assert not unescaped, f"unescaped '&' in widget labels: {unescaped}" # the two labels that actually contain an ampersand render it exactly once... - raw_direct = w.simulated.raw_direct + raw_direct = simulated.simulated.raw_direct assert raw_direct.text() == "Skip filtering && regrid (raw → retrieval)" assert _plain(raw_direct.text()) == "Skip filtering & regrid (raw → retrieval)" units = next( @@ -1910,15 +1812,15 @@ def _readout_row(readout, name): def _run_synthetic_retrieval(w, qtbot, *, energy=None, maxiters=15): """Generate a synthetic trace and retrieve it; returns the Retrieve stage.""" - w.show_synthetic() + w.start("synthetic") w.synthetic.advance() - w.state.stage = 3 # preprocess (synchronous) + w.goto(3) # preprocess (synchronous) rs = w.stages[3] rs.state.retrieve.solver = "copra" rs.state.retrieve.maxiters = maxiters if energy is not None: rs.state.load.energy_j = energy - w.state.stage = 4 + w.goto(4) with qtbot.waitSignal(w.state.result_changed, timeout=30000): rs._run() return rs @@ -1928,8 +1830,7 @@ def test_retrieval_readout_fills_from_a_synthetic_run(qtbot): """Each curve row carries exactly the quantities that are defined for it.""" from croak.gui.readout import DASH - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) lam_peak, lam_fwhm, fwhm, power = range(4) @@ -1968,8 +1869,7 @@ def test_retrieval_readout_shows_the_truth_errors(qtbot): """ from croak.gui.readout import DASH - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) scalars = {k: v.text() for k, v in rs.readout._scalars.items()} @@ -1987,8 +1887,7 @@ def test_retrieval_readout_dashes_the_truth_errors_without_a_truth(qtbot): """An experimental load has no known pulse, so nothing is invented.""" from croak.gui.readout import DASH - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = w.stages[3] rs._update_truth_error_readout(None, None) scalars = {k: v.text() for k, v in rs.readout._scalars.items()} @@ -1999,8 +1898,7 @@ def test_retrieval_readout_peak_power_column(qtbot): """The peak-power column needs a pulse energy, and shares one SI prefix.""" from croak.gui.readout import DASH - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot, energy=1.2e-6) header = rs.readout._header["peak_power"].text() @@ -2023,18 +1921,17 @@ def test_retrieval_readout_truth_row_blank_without_a_truth( """An experimental trace has no known truth, so that row stays blank.""" from croak.gui.readout import DASH - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) st = w.state st.load.frog_path = experiment_h5 w.stages[0]._populate_frog() st.load.interaction = experiment.interaction w.stages[0]._load_preview() - st.stage = 3 + w.goto(3) rs = w.stages[3] rs.state.retrieve.solver = "copra" rs.state.retrieve.maxiters = 15 - st.stage = 4 + w.goto(4) with qtbot.waitSignal(st.result_changed, timeout=30000): rs._run() @@ -2045,8 +1942,7 @@ def test_retrieval_readout_truth_row_blank_without_a_truth( def test_retrieval_readout_progress_without_a_result(qtbot): """Per-iteration updates move iteration/error with no processed result.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = w.stages[3] rs._errors = [] rs._on_progress(7, 0.0123, 0.0100) @@ -2059,8 +1955,7 @@ def test_retrieval_readout_cleared_when_a_run_starts(qtbot, monkeypatch): from croak.gui import stage_retrieve as sr from croak.gui.readout import DASH - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) assert _readout_row(rs.readout, "R")[0] != DASH @@ -2089,8 +1984,7 @@ def test_retrieval_readout_marks_extras_fitted_or_fixed(qtbot): """τ₀ / thickness / smearing say whether the solver was free to move them.""" from croak.gui.readout import DASH - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = w.stages[3] p = rs.state.retrieve scalars = rs.readout._scalars @@ -2121,8 +2015,7 @@ class _Fitted: def test_gas_controls_reach_millibar_and_long_paths(qtbot): """Pressure resolves 1 mbar; the gas path reaches ±999 cm.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[4] # StageDispersion assert stage.gas_pressure.bounds() == (0.0, 10.0) @@ -2147,11 +2040,10 @@ def test_retrieval_readout_smearing_survives_an_shg_trace(qtbot): """ from croak.gui.readout import DASH - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() # the default synthetic trace is SHG - w.state.stage = 3 + w.goto(3) rs = w.stages[3] rs.state.retrieve.smearing = True @@ -2163,14 +2055,13 @@ def test_retrieval_readout_smearing_survives_an_shg_trace(qtbot): def test_preprocess_tau_exclude_auto_button(qtbot, experiment_h5, experiment): """'Auto' fills the baseline hold-out from the loaded trace's delay marginal.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) st = w.state st.load.frog_path = experiment_h5 w.stages[0]._populate_frog() st.load.interaction = experiment.interaction w.stages[0]._load_preview() - st.stage = 3 # preprocess + w.goto(3) # preprocess stage = w.stages[2] # the whole baseline group, Auto included, is gated on the checkbox @@ -2197,8 +2088,7 @@ def test_preprocess_tau_exclude_auto_button(qtbot, experiment_h5, experiment): def test_preprocess_tau_exclude_auto_without_a_trace(qtbot): """Auto with nothing loaded reports it rather than raising.""" - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[2] assert w.state.load_data is None stage._auto_tau_exclude() @@ -2258,8 +2148,7 @@ def test_wizard_carries_the_icon_under_a_foreign_qapplication(qtbot): window-level call is what covers an embedder (and this test suite, whose QApplication comes from qtbot). """ - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) assert not w.windowIcon().isNull() @@ -2296,9 +2185,8 @@ def test_simulated_load_adopts_recorded_geometry(qtbot, simulated_truth, tmp_pat zsave=SIMULATED_ZSAVE, geometry=(1e-3, 1e-3, "tg"), ) - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated stage = w.stages[3] # StageRetrieve # start from settings that do NOT match the file, so adoption is visible @@ -2330,9 +2218,8 @@ def test_simulated_load_says_when_the_mask_is_not_recorded( """A file without mask metadata sets the thickness and says the mask is unknown.""" from conftest import SIMULATED_ZSAVE - w = Wizard() - qtbot.addWidget(w) - w.load_simulated() + w = Driver(qtbot) + w.start("simulated") sim = w.simulated stage = w.stages[3] stage.hole_spacing_spin.setValue(0.5) @@ -2360,8 +2247,7 @@ def test_solver_switch_rescales_regularisation(qtbot): """ from croak.session.params import REG_DEFAULTS - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) stage = w.stages[3] p = stage.state.retrieve grad_spec, grad_amp = REG_DEFAULTS["gradient"] @@ -2392,9 +2278,8 @@ def test_solver_switch_rescales_regularisation(qtbot): ) def test_canvas_density_follows_the_window_size(qtbot, size, small): """Plot text shrinks in a small window and stays at full size in a large one.""" - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() # marginal check: two stacked (5, 4)-inch canvases canvas = w.stages[1].marg_canvas w.resize(*size) @@ -2496,8 +2381,7 @@ def test_new_window_is_clamped_to_the_screen(qtbot): screen = QApplication.primaryScreen() assert screen is not None available = screen.availableGeometry().size() # 800x800 offscreen - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) assert w.width() <= settings.SCREEN_FILL * available.width() assert w.height() <= settings.SCREEN_FILL * available.height() assert w.width() < settings.DEFAULT_WINDOW_SIZE.width() @@ -2508,8 +2392,7 @@ def test_window_geometry_round_trips_and_is_clamped_on_restore(qtbot, layout_min """Closing saves geometry; restoration also clamps oversized saved geometry.""" from PySide6.QtWidgets import QMainWindow - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) # Windows font metrics can make the layout wider than the offscreen display. w.centralWidget().setMinimumWidth(layout_min_width) w.resize(700, 600) # the width is clamped up to the window's minimum @@ -2518,8 +2401,7 @@ def test_window_geometry_round_trips_and_is_clamped_on_restore(qtbot, layout_min shown = (w.width(), w.height()) assert shown != (settings.DEFAULT_WINDOW_SIZE.width(), 600) w.close() - again = Wizard() - qtbot.addWidget(again) + again = Driver(qtbot) again.centralWidget().setMinimumWidth(layout_min_width) # Compare at the same lifecycle stage: restoreGeometry clamps to the screen # before show() applies the platform's layout minimums. @@ -2533,8 +2415,7 @@ def test_window_geometry_round_trips_and_is_clamped_on_restore(qtbot, layout_min big.show() qtbot.waitExposed(big) settings.save_window(big) - clamped = Wizard() - qtbot.addWidget(clamped) + clamped = Driver(qtbot) assert clamped.width() <= 800 assert clamped.height() <= 800 @@ -2542,9 +2423,8 @@ def test_window_geometry_round_trips_and_is_clamped_on_restore(qtbot, layout_min # -- adaptive layout: splitter and strips ---------------------------------------- def _marginal_stage_shown(qtbot, width, height): """A wizard on the marginal-check stage, shown at ``width`` x ``height``.""" - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() w.resize(width, height) w.show() @@ -2568,11 +2448,10 @@ def test_no_stage_needs_more_than_a_small_display(qtbot): """No page (or widget on it) demands a window wider or taller than 1366x768.""" from PySide6.QtWidgets import QWidget - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() - for page in [*w.stages, w.synthetic, w.simulated, w.welcome]: + for page in [*w.stages, w.win.home]: w.stack.setCurrentWidget(page) hint = w.minimumSizeHint() assert (hint.width(), hint.height()) <= (1366, 768), type(page).__name__ @@ -2631,8 +2510,7 @@ def start(self): def test_retrieve_pages_show_three_tabs_and_draw_the_visible_page(qtbot): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) pages = rs.pages assert pages.page_keys() == ("traces", "pulse", "diagnostics") @@ -2643,8 +2521,7 @@ def test_retrieve_pages_show_three_tabs_and_draw_the_visible_page(qtbot): def test_retrieve_pages_redraw_a_stale_page_when_selected(qtbot): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) rs.pages.set_current_page("pulse") assert not rs.pages.is_stale("pulse") @@ -2654,8 +2531,7 @@ def test_retrieve_pages_redraw_a_stale_page_when_selected(qtbot): def test_retrieve_pages_pop_out_opens_a_window_that_follows_the_result(qtbot): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) dlg = rs.pages.pop_out("pulse") assert dlg.isWindow() @@ -2670,12 +2546,11 @@ def test_retrieve_pages_pop_out_opens_a_window_that_follows_the_result(qtbot): def test_retrieve_progress_curve_shows_on_the_visible_page(qtbot): - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() - w.state.stage = 3 - w.state.stage = 4 + w.goto(3) + w.goto(4) rs = w.stages[3] rs.pages.set_current_page("pulse") rs._errors = [] @@ -2696,8 +2571,7 @@ def test_retrieve_progress_curve_shows_on_the_visible_page(qtbot): def test_retrieve_run_clears_the_pages(qtbot, monkeypatch): from croak.gui import stage_retrieve as sr - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) assert rs.pages.canvas("traces").figure.axes monkeypatch.setattr(sr, "RetrievalWorker", _IdleWorker) @@ -2707,8 +2581,7 @@ def test_retrieve_run_clears_the_pages(qtbot, monkeypatch): def test_retrieve_readout_collapse_frees_canvas_height(qtbot): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) w.resize(1366, 768) w.show() @@ -2725,8 +2598,7 @@ def test_retrieve_save_writes_the_full_overview_pdf(qtbot, tmp_path, monkeypatch from croak.gui import stage_retrieve as sr - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) monkeypatch.setattr( QFileDialog, "getExistingDirectory", lambda *a, **k: str(tmp_path) @@ -2752,8 +2624,7 @@ def test_retrieve_live_preview_updates_the_page_in_place(qtbot): """A later snapshot of the same run reuses the drawn axes and artists.""" from dataclasses import replace - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) r1 = w.state.result assert r1 is not None @@ -2777,8 +2648,7 @@ def test_retrieve_live_preview_updates_the_page_in_place(qtbot): def test_retrieve_post_filter_updates_in_place(qtbot): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) figure = rs.pages.canvas("traces").figure axes = list(figure.axes) @@ -2791,12 +2661,11 @@ def test_retrieve_post_filter_updates_in_place(qtbot): def test_retrieve_convergence_redraw_is_throttled(qtbot, monkeypatch): """A thousand iterations in a burst draw the curve once, after the debounce.""" - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() + w = Driver(qtbot) + w.start("synthetic") w.synthetic.advance() - w.state.stage = 3 - w.state.stage = 4 + w.goto(3) + w.goto(4) rs = w.stages[3] calls = [] monkeypatch.setattr( @@ -2822,8 +2691,7 @@ def _spectrum_axes(rs): def test_retrieve_spectrum_axis_defaults_to_frequency_and_is_remembered(qtbot): - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) assert rs.spectrum_axis_combo.currentText() == "frequency" rs.pages.set_current_page("pulse") @@ -2831,8 +2699,7 @@ def test_retrieve_spectrum_axis_defaults_to_frequency_and_is_remembered(qtbot): rs.spectrum_axis_combo.setCurrentText("wavelength") # re-presents the result assert _spectrum_axes(rs).get_xlabel() == "Wavelength (nm)" assert settings.text("retrieve/spectrum_axis", "frequency") == "wavelength" - again = Wizard() - qtbot.addWidget(again) + again = Driver(qtbot) assert again.stages[3].spectrum_axis_combo.currentText() == "wavelength" @@ -2844,57 +2711,10 @@ def __init__(self): def report_preview_cost(self, seconds): self.costs.append(seconds) - w = Wizard() - qtbot.addWidget(w) + w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) rs._worker = _Recorder() rs._on_preview(w.state.result) rs._on_preview(w.state.result) # the second one is an in-place update assert len(rs._worker.costs) == 2 assert all(cost > 0 for cost in rs._worker.costs) - - -# -- leaving for the welcome menu ------------------------------------------------- -def test_back_to_the_menu_asks_before_leaving_a_session_with_data(qtbot, monkeypatch): - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() - w.synthetic.advance() # trace generated: on the marginal check, data in hand - w.go_back() # to the generator page: nothing is lost, nothing is asked - assert w.stack.currentWidget() is w.synthetic - asked = [] - monkeypatch.setattr(w, "_confirm_discard", lambda: asked.append("no") or False) - w.go_back() # entry page -> menu would leave the session: ask; cancelled - assert asked == ["no"] - assert w.stack.currentWidget() is w.synthetic - assert w.state.load_data is not None - monkeypatch.setattr(w, "_confirm_discard", lambda: asked.append("yes") or True) - w.go_back() - assert asked == ["no", "yes"] - assert w.stack.currentIndex() == 0 # the welcome menu - assert w.state.load_data is not None # kept until a menu entry starts anew - - -def test_back_from_an_empty_stage_one_does_not_ask(qtbot, monkeypatch): - w = Wizard() - qtbot.addWidget(w) - w.start_load() - assert not w.state.has_data - monkeypatch.setattr( - w, "_confirm_discard", lambda: pytest.fail("asked with nothing to lose") - ) - w.go_back() - assert w.stack.currentIndex() == 0 - - -def test_main_menu_button_asks_when_the_session_holds_data(qtbot, monkeypatch): - w = Wizard() - qtbot.addWidget(w) - w.show_synthetic() - w.synthetic.advance() - monkeypatch.setattr(w, "_confirm_discard", lambda: False) - w.menu_btn.click() - assert w.stack.currentWidget() is w.stages[1] # stayed on the marginal check - monkeypatch.setattr(w, "_confirm_discard", lambda: True) - w.menu_btn.click() - assert w.stack.currentIndex() == 0 diff --git a/tests/test_session_files.py b/tests/test_session_files.py index c98d5de..7068f2d 100644 --- a/tests/test_session_files.py +++ b/tests/test_session_files.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import replace +from types import SimpleNamespace import numpy as np import pytest @@ -117,3 +118,52 @@ def test_format_options_matches_the_saved_file(tmp_path, session): save_session(tmp_path, options) text = (tmp_path / OPTIONS_FILE).read_text(encoding="utf-8") assert text == save.format_options(options.to_dict()) + + +# -- result_mismatch on minimal stand-ins ---------------------------------------- +def _td(trace, omega): + return SimpleNamespace(trace=trace, grid=SimpleNamespace(omega=omega)) + + +@pytest.mark.parametrize( + ("saved", "reason"), + [ + ({}, "the saved result does not store the trace it was fitted to"), + ( + {"trace_meas": np.ones((2, 2))}, + "the saved result is on a 2x2 grid but this trace regrids to 2x3", + ), + ( + {"trace_meas": np.full((2, 3), 2.0)}, + "preprocessing no longer reproduces the trace the result was fitted to", + ), + ( + {"trace_meas": np.ones((2, 3)), "omega": np.arange(3.0)}, + "the saved result is on a different frequency grid", + ), + ( + {"trace_meas": np.ones((2, 3)), "omega": np.array([1.0, 2.5])}, + "the saved result is on a different frequency grid", + ), + ( + { + "trace_meas": np.ones((2, 3)), + "omega": np.array([1.0, 2.0]), + "algorithm": "lm", + }, + "the saved result came from lm but the loaded settings select copra", + ), + ({"trace_meas": np.ones((2, 3)), "omega": np.array([1.0, 2.0])}, ""), + ({"trace_meas": np.ones((2, 3)), "algorithm": "copra"}, ""), + ], +) +def test_session_files_mismatch_explains_why_a_result_is_refused(saved, reason): + td = _td(np.ones((2, 3)), np.array([1.0, 2.0])) + assert result_mismatch(saved, td, SimpleNamespace(solver="copra")) == reason + + +def test_session_files_mismatch_handles_an_all_zero_trace(): + # a zero peak/span falls back to an absolute tolerance of 1e-9 + td = _td(np.zeros((1, 2)), np.zeros(2)) + saved = {"trace_meas": np.zeros((1, 2)), "omega": np.zeros(2)} + assert result_mismatch(saved, td, SimpleNamespace(solver="copra")) == "" From d7e29e820cca9350d68dac206ccb81555a77a335 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 22:36:58 +0000 Subject: [PATCH 08/41] Draw the retrieval on pyqtgraph and give Analyse its result pages croak.plotting.screen turns a RetrievalPlotData into map and line views (evenly spaced images, curves with roles, ranges, notes) from the panels' own helpers, so the screen and an exported figure show the same numbers. retrieval_views draws them: Retrieve's live overview (traces, pulse, spectrum, or the convergence curve before the first preview) and Analyse's Overview, Traces, Pulse and Diagnostics pages, each with Export figure (via matplotlib) and Pop out. Maps start white and show negatives in red; trace maps share their axes; log axes span whole decades. panel_pages goes. Co-Authored-By: Claude Opus 5.5 --- croak/gui/pages.py | 14 +- croak/gui/panel_pages.py | 285 ----------------- croak/gui/plots.py | 67 +++- croak/gui/retrieval_views.py | 384 +++++++++++++++++++++++ croak/gui/stage_retrieve.py | 13 +- croak/plotting/screen.py | 491 ++++++++++++++++++++++++++++++ tests/gui/test_panel_pages.py | 85 ------ tests/gui/test_plots.py | 36 ++- tests/gui/test_retrieval_views.py | 173 +++++++++++ tests/gui/test_wizard_flow.py | 134 +++----- tests/test_plotting_screen.py | 162 ++++++++++ 11 files changed, 1364 insertions(+), 480 deletions(-) delete mode 100644 croak/gui/panel_pages.py create mode 100644 croak/gui/retrieval_views.py create mode 100644 croak/plotting/screen.py delete mode 100644 tests/gui/test_panel_pages.py create mode 100644 tests/gui/test_retrieval_views.py create mode 100644 tests/test_plotting_screen.py diff --git a/croak/gui/pages.py b/croak/gui/pages.py index e7bceac..283ba0f 100644 --- a/croak/gui/pages.py +++ b/croak/gui/pages.py @@ -4,7 +4,8 @@ pages from the shared state. Data is two steps on one page: the entry's own loader (a measured trace, a simulated scan or a synthetic pulse) and then the marginal check of the loaded trace. Analyse is a rail of pages that read the -retrieved result. +retrieved result: its panels (Overview, Traces, Pulse, Diagnostics), then +Dispersion and Uncertainty. """ from __future__ import annotations @@ -27,6 +28,7 @@ ) from .base import Stage from .help import HelpDialog +from .retrieval_views import ResultPage from .sheet import Segments from .stage import PageBase from .stage_dispersion import StageDispersion @@ -305,9 +307,19 @@ def show_help(self) -> HelpDialog | None: def default_pages(state: WizardState) -> Mapping[Place, PageBase]: """Build a session's Data, Prepare, Retrieve and Analyse pages.""" retrieve = StageRetrieve(state) + results: list[tuple[str, str, PageBase]] = [ + (key, title, ResultPage(state, key, retrieve.overview)) + for key, title in ( + ("overview", "Overview"), + ("traces", "Traces"), + ("pulse", "Pulse"), + ("diagnostics", "Diagnostics"), + ) + ] analyse = AnalysePage( state, [ + *results, ("dispersion", "Dispersion", StageDispersion(state)), ("uncertainty", "Uncertainty", StageUncertainty(state)), ], diff --git a/croak/gui/panel_pages.py b/croak/gui/panel_pages.py deleted file mode 100644 index 16d6a57..0000000 --- a/croak/gui/panel_pages.py +++ /dev/null @@ -1,285 +0,0 @@ -"""Tabbed pages of retrieval panels for the Retrieve stage, with pop-out windows. - -Why pages rather than one twelve-panel figure: on a 1366×768 display a single -3×4 figure leaves each axes about 124×61 px once titles, tick labels and colorbars -have taken their fixed share, while three 2×2 pages of the same panels give each -axes roughly 370×137 px in the same canvas. The panels are the headless ones in -:mod:`croak.plotting` (:data:`~croak.plotting.RETRIEVAL_PAGES`), so a page shows -exactly what :func:`~croak.plotting.plot_retrieval` shows, and the overview stays -available for export. - -Only the visible page is drawn when new data arrive; the others are marked stale -and drawn when their tab is selected. Before the first full live preview of a run -the stage shows the convergence curve instead ("convergence mode"), on whichever -page is visible. - -A page is drawn once and later results are pushed onto its artists in place -(:class:`~croak.plotting.RetrievalPageView`), so a live preview costs no layout -solve and nothing jumps between frames; the page is redrawn from scratch only -when the result's shape, its optional curves, or the canvas's text density change. -""" - -from __future__ import annotations - -from collections.abc import Mapping, Sequence - -from matplotlib.figure import Figure -from PySide6.QtCore import Qt -from PySide6.QtWidgets import QDialog, QTabWidget, QToolButton, QVBoxLayout, QWidget - -from .. import plotting -from ..plotting import ( - RETRIEVAL_PAGES, - ConvergenceArtists, - PageSpec, - PlotScale, - RetrievalPageView, - RetrievalPlotData, -) -from .canvas import MplCanvas, with_toolbar - -__all__ = ["RetrievalPages", "PagePopout", "PAGE_FIGSIZE"] - -#: Design size (inches) of one 2×2 page: the overview's default 17.0×9.45 in -#: gives each of its twelve panels this much, so a page panel is drawn at the same -#: size as its overview twin whenever the canvas has the room. -PAGE_FIGSIZE = (8.5, 6.3) - - -class PagePopout(QDialog): - """A top-level window showing one page of retrieval panels. - - Opened by :meth:`RetrievalPages.pop_out`; follows every later result the pages - widget receives and closes with the stage that owns it. - """ - - def __init__(self, page: PageSpec, parent: QWidget | None = None): - super().__init__(parent, Qt.WindowType.Window) - self.page = page - self.setWindowTitle(f"croak — {page.title}") - # Closing frees the window; the owner drops its reference on ``finished``. - self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) - self.canvas = MplCanvas(figsize=PAGE_FIGSIZE) - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(with_toolbar(self.canvas)) - self.resize(int(PAGE_FIGSIZE[0] * 100), int(PAGE_FIGSIZE[1] * 100) + 40) - - -class RetrievalPages(QWidget): - """The Retrieve stage's plot area: one tab per page of retrieval panels. - - Parameters - ---------- - pages : mapping of str to PageSpec - The pages to offer, in tab order; defaults to - :data:`~croak.plotting.RETRIEVAL_PAGES`. - parent : QWidget, optional - - Notes - ----- - Every canvas goes through :meth:`~croak.gui.canvas.MplCanvas.render`, so the - pages re-plot themselves when a resize changes their text density. Each tab - carries the matplotlib toolbar, whose Save writes *that page*; the stage's own - Save… writes the full overview. - """ - - def __init__( - self, - pages: Mapping[str, PageSpec] = RETRIEVAL_PAGES, - parent: QWidget | None = None, - ): - super().__init__(parent) - self._pages: dict[str, PageSpec] = dict(pages) - self._data: RetrievalPlotData | None = None - self._errors: Sequence[float] | None = None - self._stale: set[str] = set() - self._canvases: dict[str, MplCanvas] = {} - self._popouts: dict[str, PagePopout] = {} - # Per-canvas presentation state (tabs and pop-outs alike): the artist- - # reusing view of its page, the convergence-curve artists while a run has - # no full preview yet, and the text density the canvas was last drawn at. - self._views: dict[MplCanvas, RetrievalPageView] = {} - self._conv: dict[MplCanvas, ConvergenceArtists] = {} - self._scales: dict[MplCanvas, PlotScale] = {} - - self.tabs = QTabWidget() - self.tabs.setDocumentMode(True) # a slim tab bar: every pixel is the plot's - for key, page in self._pages.items(): - canvas = MplCanvas(figsize=PAGE_FIGSIZE) - self._canvases[key] = canvas - self.tabs.addTab(with_toolbar(canvas), page.title) - self.popout_btn = QToolButton() - self.popout_btn.setText("Pop out") - self.popout_btn.setToolTip( - "Open the current page in its own window, which follows later results." - ) - self.popout_btn.clicked.connect(lambda: self.pop_out()) - self.tabs.setCornerWidget(self.popout_btn, Qt.Corner.TopRightCorner) - self.tabs.currentChanged.connect(self._on_tab_changed) - - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.tabs) - - # -- queries ------------------------------------------------------------ - def page_keys(self) -> tuple[str, ...]: - """The page keys in tab order.""" - return tuple(self._pages) - - def current_page(self) -> str: - """Key of the page whose tab is selected.""" - return self.page_keys()[self.tabs.currentIndex()] - - def set_current_page(self, key: str) -> None: - """Select the tab of page ``key`` (drawing it if it is stale).""" - self.tabs.setCurrentIndex(self.page_keys().index(key)) - - def is_stale(self, key: str) -> bool: - """Whether page ``key`` still shows older content than the latest data.""" - return key in self._stale - - def canvas(self, key: str) -> MplCanvas: - """The canvas of page ``key``.""" - return self._canvases[key] - - @property - def data(self) -> RetrievalPlotData | None: - """The data the pages currently show, if any.""" - return self._data - - # -- content ------------------------------------------------------------ - def set_data(self, data: RetrievalPlotData) -> None: - """Show a result: draw the visible page (and pop-outs), mark the rest stale.""" - self._data = data - self._errors = None # leaves convergence mode - self._present() - - def show_progress(self, errors: Sequence[float]) -> None: - """Show the convergence curve of a run in progress instead of a result. - - ``errors`` is read live (the stage appends to it), so a re-plot after a - resize shows the latest history. - """ - self._errors = errors - self._present() - - def clear(self) -> None: - """Blank every page and forget the data (a new run is starting).""" - self._data = None - self._errors = None - self._stale = set() - self._conv.clear() - for view in self._views.values(): - view.reset() - for canvas in self._all_canvases(): - canvas.clear() - - def pop_out(self, key: str | None = None) -> PagePopout: - """Open (or raise) a window showing page ``key`` (default: the current one).""" - key = self.current_page() if key is None else key - popout = self._popouts.get(key) - if popout is None: - popout = PagePopout(self._pages[key], self) - popout.finished.connect(lambda _result, k=key: self._forget_popout(k)) - self._popouts[key] = popout - self._present_canvas(popout.canvas, key) - popout.show() - popout.raise_() - popout.activateWindow() - return popout - - # -- internals ---------------------------------------------------------- - def _all_canvases(self) -> list[MplCanvas]: - return [*self._canvases.values(), *(p.canvas for p in self._popouts.values())] - - def _forget_popout(self, key: str) -> None: - popout = self._popouts.pop(key, None) - if popout is not None: - for store in (self._views, self._conv, self._scales): - store.pop(popout.canvas, None) - - def _present_canvas(self, canvas: MplCanvas, key: str) -> None: - """Bring ``canvas`` (showing page ``key``) up to date with the current mode.""" - if self._errors is not None: - self._present_convergence(canvas, self._errors) - elif self._data is not None: - self._present_data(canvas, key, self._data) - else: - canvas.clear() - - def _present_convergence(self, canvas: MplCanvas, errors: Sequence[float]) -> None: - """Show the live error curve on ``canvas``, growing it in place if it exists. - - ``errors`` is the stage's own (growing) list, so a re-plot reads it live. - """ - artists = self._conv.get(canvas) - if ( - artists is not None - and canvas.rendered - and self._scales.get(canvas) == canvas.scale - ): - plotting.update_convergence_curve(artists, errors) - canvas.draw() # synchronous: cheap, and no second idle draw follows - return - view = self._views.get(canvas) - if view is not None: - view.reset() # the page's artists go with the cleared figure - - def plot(fig: Figure) -> None: - # Re-run by the canvas on a density change; `errors` is read live. - self._conv[canvas] = plotting.plot_convergence(fig.add_subplot(111), errors) - self._scales[canvas] = canvas.scale - - canvas.render(plot) - - def _present_data( - self, canvas: MplCanvas, key: str, data: RetrievalPlotData - ) -> None: - """Show ``data`` on ``canvas``: update in place when possible, else redraw. - - A redraw is needed when the page has never been drawn on this figure, - when the result changed shape or gained/lost optional curves - (:meth:`~croak.plotting.RetrievalPageView.can_update`), or when the - canvas's text density changed: rc sizes bind at artist creation, so an - in-place update would keep the stale size. - """ - view = self._views.get(canvas) - if view is None: - view = self._views[canvas] = RetrievalPageView(self._pages[key]) - if ( - view.attached - and view.fig is canvas.figure - and self._scales.get(canvas) == canvas.scale - and view.can_update(data) - ): - view.update(data) - # Synchronous, so the caller's timing of a preview includes the paint - # (the stage feeds it back to the worker's preview cadence) and no - # second idle draw follows. - canvas.draw() - return - self._conv.pop(canvas, None) - - def plot(fig: Figure) -> None: - # Re-run by the canvas on a density change: draw the latest result. - # clear() makes every canvas forget this function, so the pages still - # hold data whenever it runs; ``data`` only satisfies the type. - view.draw(fig, self._data if self._data is not None else data) - self._scales[canvas] = canvas.scale - - canvas.render(plot) - - def _present(self) -> None: - """Bring the visible page and every pop-out up to date; mark the rest stale.""" - current = self.current_page() - self._stale = set(self._pages) - {current} - self._present_canvas(self._canvases[current], current) - for key, popout in self._popouts.items(): - self._present_canvas(popout.canvas, key) - - def _on_tab_changed(self, index: int) -> None: - key = self.page_keys()[index] - if key in self._stale: - self._present_canvas(self._canvases[key], key) - self._stale.discard(key) diff --git a/croak/gui/plots.py b/croak/gui/plots.py index 1be0de4..44cadfb 100644 --- a/croak/gui/plots.py +++ b/croak/gui/plots.py @@ -30,6 +30,7 @@ "LinePlot", "MapPlot", "Plot", + "colormap", "curve_reader", "map_reader", "signed_colormap", @@ -199,6 +200,24 @@ def leaveEvent(self, event: object) -> None: # noqa: N802 — Qt override # py super().leaveEvent(event) # pyright: ignore[reportArgumentType] +def colormap(name: str) -> pg.ColorMap: + """Return the colour map ``name``: pyqtgraph's own, else matplotlib's. + + Raises + ------ + ValueError + If neither knows the name. + """ + for source in (None, "matplotlib"): + try: + # Either source raises for a name it lacks (pyqtgraph looks for a + # file of that name); the stubs allow None, which it never returns. + return cast(pg.ColorMap, pg.colormap.get(name, source=source)) + except FileNotFoundError, ValueError: + continue + raise ValueError(f"unknown colour map {name!r}") + + def signed_colormap(name: str, low: float, high: float) -> pg.ColorMap: """Return colour map ``name`` for levels ``(low, high)``, white at zero. @@ -227,12 +246,7 @@ def signed_colormap(name: str, low: float, high: float) -> pg.ColorMap: """ if not high > 0 or not low < high: raise ValueError(f"need low < high and high > 0, got ({low}, {high})") - try: - base = pg.colormap.get(name) - except FileNotFoundError: # pyqtgraph looks the name up as a file - base = None - if base is None: - raise ValueError(f"unknown colour map {name!r}") + base = colormap(name) stops = np.linspace(0.0, 1.0, 9) colours = np.array(base.map(stops, mode=pg.ColorMap.FLOAT), dtype=float) colours[0] = (1.0, 1.0, 1.0, 1.0) @@ -273,6 +287,9 @@ class MapPlot(Plot): The colour bar's label. signed : bool Draw values below zero in red fading to white (:func:`signed_colormap`). + white : bool + Start the colour map at white, so an empty background reads as paper, + as the matplotlib figures do (off for a diverging map such as ``bwr``). """ def __init__( @@ -283,17 +300,19 @@ def __init__( colours: str = "viridis", unit: str = "", signed: bool = False, + white: bool = True, ) -> None: super().__init__(xlabel=xlabel, ylabel=ylabel) self.colours = colours self.signed = signed + self.white = white self.data: np.ndarray | None = None self.x_axis = np.zeros(0) self.y_axis = np.zeros(0) self.image = _SmoothImage(axisOrder="row-major") self.addItem(self.image) self.bar = pg.ColorBarItem( - colorMap=pg.colormap.get(colours), + colorMap=signed_colormap(colours, 0.0, 1.0) if white else colormap(colours), interactive=False, width=10, colorMapMenu=False, @@ -356,8 +375,10 @@ def update_map(self, data: np.ndarray) -> None: def set_levels(self, levels: tuple[float, float]) -> None: """Change the colour bar's span without touching the data.""" low, high = float(levels[0]), float(levels[1]) - if self.signed and high > 0 and low < high: + if self.signed and high > 0 and low < 0: self.bar.setColorMap(signed_colormap(self.colours, low, high)) + elif self.signed: + self.bar.setColorMap(signed_colormap(self.colours, 0.0, 1.0)) self.bar.setLevels(values=(low, high)) def value_at(self, x: float, y: float) -> float | None: @@ -397,6 +418,10 @@ class Curve: Draw dashed. right : bool Plot against the right-hand axis (e.g. a phase beside an intensity). + width : float + Line width, logical px. + under : bool + Draw beneath the other curves (a reference such as a known pulse). """ x: np.ndarray @@ -405,6 +430,8 @@ class Curve: colour: str = "" dashed: bool = False right: bool = False + width: float = PEN_WIDTH + under: bool = False class LinePlot(Plot): @@ -473,7 +500,9 @@ def set_curves(self, curves: Sequence[Curve], *, log_y: bool = False) -> None: self.item.setLogMode(False, log_y) named = [c for c in curves if c.name] if len(named) > 1 and self.legend is None: - self.legend = self.item.addLegend(offset=(-8, 8), labelTextSize="8pt") + self.legend = self.item.addLegend( + offset=(-8, 8), labelTextSize="8pt", brush=(255, 255, 255, 210) + ) if self.legend is not None: self.legend.setVisible(len(named) > 1) auto = 0 @@ -481,9 +510,10 @@ def set_curves(self, curves: Sequence[Curve], *, log_y: bool = False) -> None: colour = curve.colour or COLOURS[auto % len(COLOURS)] auto += not curve.colour style = Qt.PenStyle.DashLine if curve.dashed else Qt.PenStyle.SolidLine - pen = pg.mkPen(colour, width=PEN_WIDTH, style=style) + pen = pg.mkPen(colour, width=curve.width, style=style) x, y = self._points(curve, curve.x, curve.y) item = pg.PlotDataItem(x, y, pen=pen) + item.setZValue(-1 if curve.under else 0) if curve.right and self.right_box is not None: self.right_box.addItem(item) else: @@ -498,6 +528,11 @@ def update_curve(self, index: int, x: np.ndarray, y: np.ndarray) -> None: self.curves[index] = curve self.items[index].setData(*self._points(curve, curve.x, curve.y)) + def set_right_label(self, text: str) -> None: + """Change the right-hand axis's label (no-op without a right axis).""" + if self.right_box is not None: + _set_label(self.item.getAxis("right"), text) + def set_right_range(self, low: float, high: float) -> None: """Fix the right-hand axis's range.""" if self.right_box is not None: @@ -526,11 +561,17 @@ def fit(self) -> None: x = np.concatenate([d[0] for d in drawn]) y = np.concatenate([d[1] for d in drawn]) y = y[np.isfinite(y)] - if self.log_y: - y = np.log10(y) + xr = (float(np.min(x)), float(np.max(x))) + if self.log_y and y.size: + # Whole decades, as matplotlib's log axes: inside one decade + # pyqtgraph has no tick to label. + low = float(np.floor(np.log10(np.min(y)))) + high = max(float(np.ceil(np.log10(np.max(y)))), low + 1.0) + self.set_view(xr, (low, high)) + return yr = (float(np.min(y)), float(np.max(y))) if y.size else (0.0, 1.0) pad = 0.05 * ((yr[1] - yr[0]) or 1.0) - self.set_view((float(np.min(x)), float(np.max(x))), (yr[0] - pad, yr[1] + pad)) + self.set_view(xr, (yr[0] - pad, yr[1] + pad)) def map_reader( diff --git a/croak/gui/retrieval_views.py b/croak/gui/retrieval_views.py new file mode 100644 index 0000000..a73aa0f --- /dev/null +++ b/croak/gui/retrieval_views.py @@ -0,0 +1,384 @@ +"""The retrieval's panels on screen, in pyqtgraph. + +:class:`RetrievalPanels` shows a grid of the retrieval's panels, named by the +same keys as the matplotlib figure (:data:`croak.plotting.RETRIEVAL_PANELS`), +from the screen data of :mod:`croak.plotting.screen`. A live retrieval updates +it in place; before the first preview it shows the convergence curve instead. +:class:`ResultPage` puts a grid on Analyse's rail, with export and pop-out. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import cast + +import numpy as np +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QDialog, + QFileDialog, + QGridLayout, + QHBoxLayout, + QMenu, + QStackedWidget, + QToolButton, + QVBoxLayout, + QWidget, +) + +from ..plotting import ( + RETRIEVAL_PAGES, + PageSpec, + RetrievalPlotData, + plot_retrieval_page, +) +from ..plotting import screen as sc +from .plots import ( + COLOURS, + PEN_WIDTH, + Curve, + LinePlot, + MapPlot, + curve_reader, + map_reader, +) +from .stage import PageBase +from .state import WizardState +from .theme import PALETTE, label, set_flag + +__all__ = [ + "OVERVIEW", + "PANELS", + "Panel", + "ResultPage", + "RetrievalPanels", +] + +OVERVIEW: tuple[tuple[str, ...], ...] = ( + ("measured_log", "retrieved_log"), + ("temporal", "spectral"), +) +"""The live overview: the traces above the pulse and its spectrum.""" + +# Each curve role's colour, whether it is dashed, and whether it is a thin +# reference drawn beneath the rest (screen.ROLES), as in the matplotlib figures. +ROLE_STYLE: Mapping[str, tuple[str, bool, bool]] = { + "retrieved": (COLOURS[0], False, False), + "transform-limited": (COLOURS[1], False, False), + "measured": (COLOURS[1], False, False), + "truth": (PALETTE.ink, False, True), + "phase": (COLOURS[2], False, False), + "fit": (COLOURS[3], True, False), + "truth-phase": (PALETTE.muted, True, True), + "error": (COLOURS[0], False, False), +} +REFERENCE_WIDTH = 0.9 +"""The width of a reference curve (the known pulse), logical px.""" + + +@dataclass(frozen=True) +class Panel: + """One panel: how to build its view, and how to draw it.""" + + view: Callable[[RetrievalPlotData], sc.MapView | sc.LineView | None] + kind: str # "map" or "line" + colours: str = "viridis" + signed: bool = False + white: bool = True + right: bool = False + note: Callable[[RetrievalPlotData], str] | None = None + + +def _retrieved_note(data: RetrievalPlotData) -> str: + rows, cols = data.retrieved.shape + return f"R {data.result.error:.2%} · {rows}×{cols}" + + +PANELS: Mapping[str, Panel] = { + "measured_log": Panel(lambda d: sc.trace_map(d, "measured", db=True), "map"), + "retrieved_log": Panel(lambda d: sc.trace_map(d, "retrieved", db=True), "map"), + "measured_lin": Panel( + lambda d: sc.trace_map(d, "measured", db=False), "map", signed=True + ), + "retrieved_lin": Panel( + lambda d: sc.trace_map(d, "retrieved", db=False), + "map", + signed=True, + note=_retrieved_note, + ), + "temporal": Panel(sc.temporal_view, "line", right=True), + "spectral": Panel(sc.spectral_view, "line", right=True), + "delay_marginal": Panel(sc.delay_marginal_view, "line"), + "freq_marginal": Panel(sc.frequency_marginal_view, "line"), + "convergence": Panel( + lambda d: sc.convergence_view(d.processed.errors, d.result.stage_boundaries), + "line", + ), + "residual": Panel(sc.residual_map, "map", colours="bwr", white=False), + "spectral_filter": Panel(sc.filter_view, "line"), + "spectrogram": Panel(sc.spectrogram_map, "map"), +} +"""The panels the screen can show, by the figure's keys.""" + + +def draw_line_view(plot: LinePlot, view: sc.LineView) -> None: + """Show ``view`` on ``plot``: curves, labels, ranges and marks.""" + curves = [] + for c in view.curves: + colour, dashed, under = ROLE_STYLE[c.role] + width = REFERENCE_WIDTH if under else PEN_WIDTH + curves.append(Curve(c.x, c.y, c.name, colour, dashed, c.right, width, under)) + plot.set_labels(view.xlabel, view.ylabel) + if view.right_label is not None: + plot.set_right_label(view.right_label) + plot.set_curves(curves, log_y=view.log_y) + plot.fit() + if view.xrange is not None or view.yrange is not None: + (x0, x1), (y0, y1) = plot.box.viewRange() + xr = view.xrange or (x0, x1) + yr = view.yrange if view.yrange is not None and not view.log_y else (y0, y1) + plot.set_view(xr, yr) + if view.right_range is not None: + plot.set_right_range(*view.right_range) + plot.set_marks(view.marks, PALETTE.muted) + + +def draw_map_view(plot: MapPlot, view: sc.MapView) -> None: + """Show ``view`` on ``plot``, then narrow it to the view's window.""" + plot.set_labels(view.xlabel, view.ylabel) + plot.show_map(view.values, view.x, view.y, view.levels) + (x0, x1), (y0, y1) = plot.box.viewRange() + plot.set_view(view.xrange or (x0, x1), view.yrange or (y0, y1)) + + +class PanelBox(QWidget): + """A titled panel: its plot, with a line of notes under the title.""" + + def __init__(self, key: str) -> None: + super().__init__() + self.key = key + self.spec = PANELS[key] + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + head = QHBoxLayout() + self.title = label("", "plotTitle") + self.notes = label("", "pageNote") + self.notes.setAlignment(Qt.AlignmentFlag.AlignRight) + head.addWidget(self.title) + head.addStretch(1) + head.addWidget(self.notes) + layout.addLayout(head) + self.plot: MapPlot | LinePlot + if self.spec.kind == "map": + plot = MapPlot( + colours=self.spec.colours, + signed=self.spec.signed, + white=self.spec.white, + ) + plot.reader = map_reader(plot, "x", "y") + self.plot = plot + else: + line = LinePlot(right_label="" if self.spec.right else None) + line.reader = curve_reader(line, "x") + self.plot = line + layout.addWidget(self.plot, stretch=1) + + def show_data(self, data: RetrievalPlotData) -> None: + """Draw this panel's view of ``data``.""" + view = self.spec.view(data) + if view is None: # only the residual can be missing + self.title.setText("Residuals") + cast(MapPlot, self.plot).say("No residual for this result.") + self.notes.setText("") + return + self.title.setText(view.title) + if isinstance(view, sc.MapView): + draw_map_view(cast(MapPlot, self.plot), view) + notes = self.spec.note(data) if self.spec.note is not None else "" + self.notes.setText(notes) + set_flag(self.notes, "warn", False) + return + line = cast(LinePlot, self.plot) + draw_line_view(line, view) + self.notes.setText(" ".join(text for text, _ in view.notes)) + set_flag(self.notes, "warn", any(warn for _, warn in view.notes)) + + +class RetrievalPanels(QWidget): + """A grid of retrieval panels, with a convergence view for a run's start. + + Parameters + ---------- + mosaic : sequence of sequence of str + Rows of panel keys (:data:`PANELS`). + """ + + def __init__(self, mosaic: Sequence[Sequence[str]] = OVERVIEW) -> None: + super().__init__() + self.stack = QStackedWidget() + grid_widget = QWidget() + grid = QGridLayout(grid_widget) + grid.setContentsMargins(0, 0, 0, 0) + grid.setHorizontalSpacing(14) + grid.setVerticalSpacing(10) + self.boxes: dict[str, PanelBox] = {} + for r, row in enumerate(mosaic): + for c, key in enumerate(row): + box = PanelBox(key) + grid.addWidget(box, r, c) + self.boxes[key] = box + self._link_maps() + self.convergence = LinePlot(xlabel="Iteration", ylabel="Error") + self.convergence.reader = curve_reader(self.convergence, "iteration") + conv = QWidget() + cl = QVBoxLayout(conv) + cl.setContentsMargins(0, 0, 0, 0) + cl.addWidget(label("Convergence", "plotTitle")) + cl.addWidget(self.convergence, stretch=1) + self.empty = label("Run a retrieval to see it here.", "note") + self.empty.setAlignment(Qt.AlignmentFlag.AlignCenter) + for widget in (self.empty, conv, grid_widget): + self.stack.addWidget(widget) + self.grid_widget = grid_widget + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.stack) + self.data: RetrievalPlotData | None = None + + def _link_maps(self) -> None: + """Trace maps share delay and frequency axes: zooming one zooms all.""" + maps = [ + b.plot + for k, b in self.boxes.items() + if k in ("measured_log", "retrieved_log", "measured_lin", "retrieved_lin") + ] + for other in maps[1:]: + other.setXLink(maps[0]) + other.setYLink(maps[0]) + + @property + def mode(self) -> str: + """``"empty"``, ``"progress"`` or ``"panels"``.""" + return ("empty", "progress", "panels")[self.stack.currentIndex()] + + def set_data(self, data: RetrievalPlotData) -> None: + """Show ``data`` on every panel (cheap enough for a live preview).""" + self.data = data + for box in self.boxes.values(): + box.show_data(data) + self.stack.setCurrentWidget(self.grid_widget) + + def show_progress(self, errors: Sequence[float]) -> None: + """Before the first preview of a run: the error so far against iteration.""" + view = sc.convergence_view(np.asarray(errors, dtype=float)) + draw_line_view(self.convergence, view) + self.stack.setCurrentIndex(1) + + def clear(self) -> None: + """Forget the shown result.""" + self.data = None + self.stack.setCurrentWidget(self.empty) + + +class ResultPage(PageBase): + """An Analyse page: one of the figure's pages of panels, with export. + + Parameters + ---------- + state : WizardState + The session. + key : str + The figure page (:data:`croak.plotting.RETRIEVAL_PAGES`), or + ``"overview"`` for :data:`OVERVIEW`. + plot_data : callable + Returns the current retrieval's plot data, or ``None`` without one. + """ + + def __init__( + self, + state: WizardState, + key: str, + plot_data: Callable[[], RetrievalPlotData | None], + ) -> None: + super().__init__(state) + self.key = key + self.plot_data = plot_data + self.mosaic = OVERVIEW if key == "overview" else RETRIEVAL_PAGES[key].mosaic + self.title = "Overview" if key == "overview" else RETRIEVAL_PAGES[key].title + self.popouts: list[QDialog] = [] + layout = QVBoxLayout(self) + layout.setContentsMargins(16, 10, 16, 10) + layout.setSpacing(8) + head = QHBoxLayout() + head.addWidget(label(self.title, "h2")) + head.addStretch(1) + self.export = QToolButton() + self.export.setObjectName("plain") + self.export.setText("Export ▾") + self.export.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + menu = QMenu(self.export) + menu.addAction("Figure…", self.export_figure) + menu.addAction("Pop out", self.pop_out) + self.export.setMenu(menu) + head.addWidget(self.export) + layout.addLayout(head) + self.panels = RetrievalPanels(self.mosaic) + layout.addWidget(self.panels, stretch=1) + state.result_changed.connect(self._stale) + + def _stale(self) -> None: + """Redraw now if on screen; otherwise when next shown.""" + if self.isVisible(): + self.on_enter() + + def on_enter(self) -> None: + data = self.plot_data() + if data is None: + self.panels.clear() + return + self.panels.set_data(data) + for dialog in self.popouts: + cast(RetrievalPanels, dialog.findChild(RetrievalPanels)).set_data(data) + + def pop_out(self) -> QDialog: + """Open this page in a window of its own that follows later results.""" + dialog = QDialog(self) + dialog.setWindowTitle(f"croak — {self.title}") + dialog.resize(900, 640) + layout = QVBoxLayout(dialog) + panels = RetrievalPanels(self.mosaic) + layout.addWidget(panels) + data = self.plot_data() + if data is not None: + panels.set_data(data) + dialog.finished.connect(lambda _code: self.popouts.remove(dialog)) + self.popouts.append(dialog) + dialog.show() + return dialog + + def export_figure(self) -> str: + """Save this page as a figure (PDF, PNG or SVG) drawn by matplotlib.""" + data = self.plot_data() + if data is None: + self.message.emit("Run a retrieval first: there is no figure to save.") + return "" + path, _ = QFileDialog.getSaveFileName( + self, + f"Save {self.title.lower()} as a figure", + f"{self.key}.pdf", + "PDF (*.pdf);;PNG (*.png);;SVG (*.svg)", + ) + if not path: + return "" + spec = ( + PageSpec("overview", "Overview", self.mosaic) + if self.key == "overview" + else RETRIEVAL_PAGES[self.key] + ) + figure = plot_retrieval_page(data, spec) + figure.savefig(path, dpi=300) + self.message.emit(f"Saved {Path(path).name}") + return path diff --git a/croak/gui/stage_retrieve.py b/croak/gui/stage_retrieve.py index c2b24b9..4ef875e 100644 --- a/croak/gui/stage_retrieve.py +++ b/croak/gui/stage_retrieve.py @@ -37,8 +37,8 @@ from . import settings from .base import RunControls, Stage from .canvas import check, collapsible, combo, dspin, group, spin -from .panel_pages import RetrievalPages from .readout import DASH, RetrievalReadout +from .retrieval_views import RetrievalPanels from .widgets import SciSpinBox from .worker import RetrievalWorker @@ -890,12 +890,11 @@ def __init__(self, state): self.controls.addWidget(self.retarget_btn) self.controls.addStretch(1) - # Figure on top (it takes every spare pixel), numeric read-out beneath it - # at its natural height — see croak.gui.readout. - # Pages of panels on top (they take every spare pixel; see - # croak.gui.panel_pages), the numeric read-out beneath at its natural - # height, folding away with a click and remembering that. - self.pages = RetrievalPages() + # The live overview on top (it takes every spare pixel; see + # croak.gui.retrieval_views), the numeric read-out beneath at its natural + # height, folding away with a click and remembering that. The full pages + # of panels are on Analyse. + self.pages = RetrievalPanels() self.readout = RetrievalReadout() self.readout_strip = collapsible( "Numeric read-out", diff --git a/croak/plotting/screen.py b/croak/plotting/screen.py new file mode 100644 index 0000000..90563a2 --- /dev/null +++ b/croak/plotting/screen.py @@ -0,0 +1,491 @@ +"""What the GUI's interactive retrieval views show, as plain data. + +The matplotlib panels (:mod:`croak.plotting.panels`) draw a retrieval for a +figure; this module turns the same :class:`RetrievalPlotData` into the arrays an +interactive view draws: :class:`MapView` for an image and :class:`LineView` for +curves. Both use the panels' own helpers, so a curve on screen is the curve in +an exported figure. Nothing here imports Qt. + +Two display choices differ from the figures. An image is resampled onto evenly +spaced axes (an interactive image is placed by its corners, a ``pcolormesh`` by +every edge), and a logarithmic trace shows the positive part only: negative +samples, from background subtraction, are visible on the linear maps, where the +signed colour map shows them in red. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass, field + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +from ..maths import wlfreq +from .panels import RetrievalPlotData, _filter_factor +from .pulse import ( + EDGE_ENERGY_WARN, + _dispersion_text, + _intensity_ylim, + _phase_ylim, + _spectral_curves, + _spectral_xlim, + _temporal_curves, + _truth_intensity, + _truth_spectral_phase, + _truth_spectrum, +) +from .units import _TWOPI_PHZ + +__all__ = [ + "CurveView", + "LineView", + "MapView", + "convergence_view", + "delay_marginal_view", + "filter_view", + "frequency_marginal_view", + "residual_map", + "spectral_view", + "spectrogram_map", + "temporal_view", + "trace_map", + "uniform_image", +] + +# Roles a view gives its curves; the GUI maps each to a colour and a line style. +ROLES = ( + "retrieved", + "transform-limited", + "measured", + "truth", + "phase", + "fit", + "truth-phase", + "error", +) + + +@dataclass(frozen=True) +class MapView: + """An image on evenly spaced axes. + + Attributes + ---------- + values : ndarray, shape (len(y), len(x)) + What is drawn, in the units of ``levels``. + x, y : ndarray + Increasing, evenly spaced sample positions. + levels : tuple of float + The colour scale's span. + title, xlabel, ylabel, unit : str + The view's title, axis labels and colour-bar label. + colours : str + The colour map's name. + signed : bool + Values below zero are meaningful (draw them in red, fading to white at 0). + xrange, yrange : tuple of float or None + The view's initial window (``None``: the whole image). + """ + + values: NDArray[np.float64] + x: NDArray[np.float64] + y: NDArray[np.float64] + levels: tuple[float, float] + title: str + xlabel: str + ylabel: str + unit: str = "" + colours: str = "viridis" + signed: bool = False + xrange: tuple[float, float] | None = None + yrange: tuple[float, float] | None = None + + +@dataclass(frozen=True) +class CurveView: + """One curve of a :class:`LineView`; ``role`` decides its colour and style.""" + + x: NDArray[np.float64] + y: NDArray[np.float64] + role: str + name: str = "" + right: bool = False + + +@dataclass(frozen=True) +class LineView: + """Curves against one x axis, with an optional right-hand axis. + + Attributes + ---------- + curves : tuple of CurveView + Left-axis curves first, then any on the right axis. + title, xlabel, ylabel : str + Labels. + right_label : str or None + The right-hand axis's label; ``None`` for none. + xrange, yrange, right_range : tuple of float or None + The initial window of each axis. + log_y : bool + A logarithmic left axis. + notes : tuple of (str, bool) + Short annotations, each with whether it is a warning. + marks : tuple of float + x positions of dashed vertical rules. + """ + + curves: tuple[CurveView, ...] + title: str + xlabel: str + ylabel: str + right_label: str | None = None + xrange: tuple[float, float] | None = None + yrange: tuple[float, float] | None = None + right_range: tuple[float, float] | None = None + log_y: bool = False + notes: tuple[tuple[str, bool], ...] = field(default_factory=tuple) + marks: tuple[float, ...] = field(default_factory=tuple) + + +def _evenly(axis: NDArray[np.float64]) -> NDArray[np.float64]: + """``axis`` if its steps are equal (to 0.1 %), else an evenly spaced copy.""" + steps = np.diff(axis) + if axis.size < 3 or np.allclose(steps, steps[0], rtol=1e-3, atol=0.0): + return axis + return np.linspace(axis[0], axis[-1], axis.size) + + +def uniform_image( + x: ArrayLike, y: ArrayLike, values: ArrayLike +) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: + """Put an image on increasing, evenly spaced axes for display. + + A decreasing axis is reversed with the image; an uneven one (a wavelength + axis from an even frequency grid, an uneven delay scan) is resampled by + linear interpolation, which moves no feature: each sample stays at its own + position. The physics arrays are untouched; this is for drawing only. + + Parameters + ---------- + x, y : array_like + The sample positions, monotonic. + values : array_like, shape (len(y), len(x)) + The image. + + Returns + ------- + tuple of ndarray + ``(x, y, values)`` on even, increasing axes. + + Raises + ------ + ValueError + If the shapes disagree or an axis has fewer than two samples. + + Examples + -------- + >>> x, y, v = uniform_image([2.0, 1.0, 0.0], [0.0, 1.0], [[3, 2, 1], [6, 5, 4]]) + >>> x.tolist(), v[0].tolist() + ([0.0, 1.0, 2.0], [1.0, 2.0, 3.0]) + """ + x = np.asarray(x, dtype=float) + y = np.asarray(y, dtype=float) + values = np.asarray(values, dtype=float) + if values.shape != (y.size, x.size): + raise ValueError(f"values {values.shape} do not match y×x {(y.size, x.size)}") + if x.size < 2 or y.size < 2: + raise ValueError("an image needs at least two samples along each axis") + if x[0] > x[-1]: + x, values = x[::-1], values[:, ::-1] + if y[0] > y[-1]: + y, values = y[::-1], values[::-1] + xe, ye = _evenly(x), _evenly(y) + if xe is not x: + values = np.array([np.interp(xe, x, row) for row in values]) + if ye is not y: + values = np.array([np.interp(ye, y, col) for col in values.T]).T + return xe, ye, values + + +def _delay_window(data: RetrievalPlotData) -> tuple[float, float]: + half = data.halfwidth / 1e-15 + return -half, half + + +def _frequency_window(data: RetrievalPlotData) -> tuple[float, float] | None: + if data.flim is None: + return None + return data.flim[0] / 1e15, data.flim[1] / 1e15 + + +def trace_map(data: RetrievalPlotData, which: str, *, db: bool) -> MapView: + """Return the measured or retrieved trace against delay and signal frequency. + + Parameters + ---------- + data : RetrievalPlotData + The retrieval. + which : {"measured", "retrieved"} + Which trace. + db : bool + Logarithmic (``data.tracedb`` dynamic range below the peak), or linear + on the scale the two linear traces share. + + Raises + ------ + ValueError + For any other ``which``. + """ + if which not in ("measured", "retrieved"): + raise ValueError(f"which must be 'measured' or 'retrieved', not {which!r}") + trace = data.measured if which == "measured" else data.retrieved + if db: + peak = float(np.nanmax(trace)) or 1.0 + with np.errstate(divide="ignore"): + shown = 10 * np.log10(np.maximum(trace, 0.0) / peak) + shown = np.clip(shown, -data.tracedb, 0.0) + levels, unit = (-data.tracedb, 0.0), "dB" + else: + shown, levels, unit = trace, (0.0, data.lin_vmax), "a.u." + x, y, values = uniform_image( + data.result.delays / 1e-15, + (data.result.omega + data.omega0_trace) / _TWOPI_PHZ, + shown, + ) + low = float(np.nanmin(values)) + if not db and low < 0: + levels = (low, data.lin_vmax) + scale = "log" if db else "lin" + return MapView( + values=values, + x=x, + y=y, + levels=levels, + title=f"{which.capitalize()} ({scale})", + xlabel="Delay (fs)", + ylabel="Frequency (PHz)", + unit=unit, + signed=not db, + xrange=_delay_window(data), + yrange=_frequency_window(data), + ) + + +def residual_map(data: RetrievalPlotData) -> MapView | None: + """Return measured minus retrieved on a symmetric scale (None without one).""" + residual = data.processed.residual + if residual is None: + return None + reach = float(np.max(np.abs(residual))) or 1.0 + x, y, values = uniform_image( + data.result.delays / 1e-15, + (data.result.omega + data.result.omega0) / _TWOPI_PHZ, + residual, + ) + return MapView( + values=values, + x=x, + y=y, + levels=(-reach, reach), + title="Residuals", + xlabel="Delay (fs)", + ylabel="Frequency (PHz)", + unit="a.u.", + colours="bwr", + xrange=_delay_window(data), + yrange=_frequency_window(data), + ) + + +def spectrogram_map(data: RetrievalPlotData) -> MapView: + """Return the retrieved pulse's Gabor spectrogram, time against wavelength.""" + t_centres, wavelength, s = data.spectrogram + x, y, values = uniform_image(t_centres / 1e-15, wavelength / 1e-9, s) + return MapView( + values=values, + x=x, + y=y, + levels=(0.0, float(np.nanmax(values)) or 1.0), + title="Spectrogram", + xlabel="Time (fs)", + ylabel="Wavelength (nm)", + unit="a.u.", + xrange=_delay_window(data), + yrange=(data.lam_min / 1e-9, data.lam_max / 1e-9), + ) + + +def temporal_view(data: RetrievalPlotData) -> LineView: + """Return the retrieved pulse: intensity or power, transform limit, phase.""" + pr = data.processed + retr, tl, r_label, tl_label, ylabel, power_scale = _temporal_curves( + pr, data.power_scale + ) + curves = [ + CurveView(pr.t_over / 1e-15, retr, "retrieved", r_label), + CurveView(pr.t_tl / 1e-15, tl, "transform-limited", tl_label), + ] + truth_curve = None + truth = data.truth + if truth is not None: + truth_curve = _truth_intensity(truth, pr.energy, power_scale) + curves.append( + CurveView( + truth.t / 1e-15, + truth_curve, + "truth", + f"truth ({truth.fwhm / 1e-15:.1f} fs)", + ) + ) + t = pr.t_over / 1e-15 + curves.append(CurveView(t[pr.mask_t], pr.phi_t[pr.mask_t], "phase", "phase", True)) + truth_phase = None + if truth is not None and truth.phi_t is not None: + keep = np.asarray(truth.It) > 0.01 + truth_phase = np.asarray(truth.phi_t)[keep] + curves.append( + CurveView( + np.asarray(truth.t)[keep] / 1e-15, + truth_phase, + "truth-phase", + "truth phase", + True, + ) + ) + return LineView( + curves=tuple(curves), + title="Retrieved pulse", + xlabel="Time (fs)", + ylabel=ylabel, + right_label="Phase (rad)", + xrange=_delay_window(data), + yrange=_intensity_ylim(retr, tl, truth_curve), + right_range=_phase_ylim(pr.phi_t[pr.mask_t], truth_phase), + ) + + +def spectral_view(data: RetrievalPlotData) -> LineView: + """Return the spectrum: retrieved, measured, phase and its fit, the truth. + + Its notes are the GDD/TOD of the phase fit and the energy at the grid's + edge, a warning above :data:`~croak.plotting.EDGE_ENERGY_WARN`. + """ + pr = data.processed + axis = data.spectral_axis + x, retrieved, measured, xlabel = _spectral_curves(pr, axis) + curves = [CurveView(x, retrieved, "retrieved", "R")] + if measured is not None: + curves.append(CurveView(x, measured, "measured", "M")) + truth = None if data.truth is None else _truth_spectrum(data.truth, axis) + if truth is not None: + curves.append(CurveView(truth[0], truth[1], "truth", "truth")) + mask = pr.mask_w + curves.append(CurveView(x[mask], pr.phi_w[mask], "phase", "phase", True)) + curves.append(CurveView(x[mask], pr.phi_w_fit[mask], "fit", "phase fit", True)) + truth_phase = _truth_spectral_phase(pr.wavelength, data.truth) + if truth_phase is not None: + curves.append( + CurveView(x[mask], truth_phase[mask], "truth-phase", "truth phase", True) + ) + edge = pr.edge_energy + return LineView( + curves=tuple(curves), + title="Spectrum", + xlabel=xlabel, + ylabel="Power (a.u.)", + right_label="Phase (rad)", + xrange=_spectral_xlim(data.lam_min, data.lam_max, axis), + yrange=(0.0, 1.05), + right_range=_phase_ylim( + pr.phi_w[mask], None if truth_phase is None else truth_phase[mask] + ), + notes=( + (_dispersion_text(pr).replace("\n", " "), False), + (f"Edge: {100 * edge:.2f} %", edge > EDGE_ENERGY_WARN), + ), + ) + + +def convergence_view(errors: ArrayLike, boundaries: Sequence[int] = ()) -> LineView: + """Return the FROG error against iteration, with the stage handovers.""" + errors = np.asarray(errors, dtype=float) + iterations = np.arange(1, errors.size + 1, dtype=float) + return LineView( + curves=(CurveView(iterations, errors, "error", "error"),), + title="Convergence", + xlabel="Iteration", + ylabel="Error", + log_y=True, + marks=tuple(float(edge) + 0.5 for edge in boundaries if 0 < edge < errors.size), + ) + + +def _marginal_view( + x: NDArray[np.float64], + retrieved: NDArray[np.float64], + measured: NDArray[np.float64] | None, + *, + title: str, + xlabel: str, + xrange: tuple[float, float] | None, +) -> LineView: + curves = [CurveView(x, retrieved, "retrieved", "R")] + if measured is not None: + curves.append(CurveView(x, measured, "measured", "M")) + return LineView( + curves=tuple(curves), + title=title, + xlabel=xlabel, + ylabel="Marginal (a.u.)", + xrange=xrange, + yrange=(0.0, 1.05), + ) + + +def frequency_marginal_view(data: RetrievalPlotData) -> LineView: + """Return the frequency marginals of the retrieved and measured traces.""" + pr = data.processed + x = (data.result.omega + data.result.omega0) / _TWOPI_PHZ + return _marginal_view( + x, + pr.omega_marg_retr, + pr.omega_marg_meas, + title="Frequency marginal", + xlabel="Frequency (PHz)", + xrange=None, + ) + + +def delay_marginal_view(data: RetrievalPlotData) -> LineView: + """Return the delay marginals of the retrieved and measured traces.""" + pr = data.processed + return _marginal_view( + data.result.delays / 1e-15, + pr.tau_marg_retr, + pr.tau_marg_meas, + title="Delay marginal", + xlabel="Delay (fs)", + xrange=_delay_window(data), + ) + + +def filter_view(data: RetrievalPlotData) -> LineView: + """Return the per-frequency scale factor (Rω filter) against wavelength.""" + result = data.result + # A centred grid can cross zero frequency, a pole of the wavelength axis. + with np.errstate(divide="ignore", invalid="ignore"): + lam_nm = wlfreq(result.omega + result.omega0) / 1e-9 + keep = np.isfinite(lam_nm) + order = np.argsort(lam_nm[keep]) + return LineView( + curves=( + CurveView( + lam_nm[keep][order], _filter_factor(result)[keep][order], "retrieved" + ), + ), + title="Spectral filter", + xlabel="Wavelength (nm)", + ylabel="Filter factor", + xrange=(data.lam_min / 1e-9, data.lam_max / 1e-9), + ) diff --git a/tests/gui/test_panel_pages.py b/tests/gui/test_panel_pages.py deleted file mode 100644 index c6e5ebf..0000000 --- a/tests/gui/test_panel_pages.py +++ /dev/null @@ -1,85 +0,0 @@ -"""RetrievalPages: pop-out windows and switching between result and convergence.""" - -from __future__ import annotations - -import numpy as np -import pytest - -pytest.importorskip("PySide6") - -from croak import plotting -from croak.forward import maketrace -from croak.grid import Grid -from croak.gui.panel_pages import RetrievalPages -from croak.maths import wlfreq -from croak.pulses import gaussian_pulse -from croak.retrieve import retrieve - - -@pytest.fixture(scope="module") -def plot_data(): - g = Grid(96, dt=0.5e-15) - ew = gaussian_pulse(g, 7e-15, phases=[20e-30]) - delays = np.linspace(-50e-15, 50e-15, 60) - trace = maketrace(g.omega, delays, ew, "pg") - res = retrieve( - trace, - g.omega, - delays, - "pg", - algorithm="copra", - maxiters=20, - guess=ew, - omega0=wlfreq(800e-9), - progress=False, - ) - return plotting.retrieval_plot_data( - res, measured=trace, lam_min=700e-9, lam_max=950e-9 - ) - - -def test_pop_out_before_any_result_shows_a_blank_page(qtbot): - pages = RetrievalPages() - qtbot.addWidget(pages) - popout = pages.pop_out() - assert popout.page.title == plotting.RETRIEVAL_PAGES[pages.current_page()].title - assert not popout.canvas.rendered - # A second request raises the same window rather than opening another. - assert pages.pop_out() is popout - - -def test_closing_a_pop_out_forgets_it(qtbot, plot_data): - pages = RetrievalPages() - qtbot.addWidget(pages) - pages.set_data(plot_data) - key = pages.page_keys()[1] - popout = pages.pop_out(key) - assert popout.canvas.rendered # drawn with the current result - with qtbot.waitSignal(popout.finished): - popout.close() - assert key not in pages._popouts - # Forgetting a window that is already gone is harmless. - pages._forget_popout(key) - assert pages.pop_out(key) is not popout - - -def test_progress_after_a_result_replaces_the_page_with_the_curve(qtbot, plot_data): - pages = RetrievalPages() - qtbot.addWidget(pages) - pages.set_data(plot_data) - key = pages.current_page() - canvas = pages.canvas(key) - view = pages._views[canvas] - assert view.attached - errors = [0.5, 0.2] - pages.show_progress(errors) - # The page's artists went with the cleared figure; the curve took over. - assert not view.attached - assert canvas in pages._conv - # The curve grows in place from the same (live) list. - artists = pages._conv[canvas] - assert len(artists.line.get_xdata()) == 2 - errors.append(0.1) - pages.show_progress(errors) - assert pages._conv[canvas] is artists - assert len(artists.line.get_xdata()) == 3 diff --git a/tests/gui/test_plots.py b/tests/gui/test_plots.py index dc81f60..22808cc 100644 --- a/tests/gui/test_plots.py +++ b/tests/gui/test_plots.py @@ -66,9 +66,13 @@ def test_plots_unsigned_colormap_starts_white(): ("viridis", 0.0, 0.0, "high > 0"), ("viridis", 2.0, 1.0, "low < high"), ("no-such-map", 0.0, 1.0, "unknown colour map"), + ("bwr", 0.0, 1.0, ""), ], ) def test_plots_signed_colormap_rejects_bad_levels_and_names(name, low, high, match): + if not match: # a matplotlib map is found too + assert signed_colormap(name, low, high) is not None + return with pytest.raises(ValueError, match=match): signed_colormap(name, low, high) @@ -121,6 +125,18 @@ def test_plots_signed_map_recolours_for_negative_levels(qtbot): plot.set_levels((-2.0, 6.0)) cmap = plot.bar.colorMap() assert_allclose(cmap.map([0.0], mode=cmap.FLOAT)[0], NEGATIVE_RGBA, atol=1 / 255) + plot.set_levels((0.0, 6.0)) # no negatives: back to white at the floor + cmap = plot.bar.colorMap() + assert_allclose(cmap.map([0.0], mode=cmap.FLOAT)[0], (1, 1, 1, 1), atol=1 / 255) + + +def test_plots_map_starts_white_unless_told_otherwise(qtbot): + white = _map(qtbot) + cmap = white.bar.colorMap() + assert_allclose(cmap.map([0.0], mode=cmap.FLOAT)[0], (1, 1, 1, 1), atol=1 / 255) + plain = _map(qtbot, white=False, colours="bwr") + cmap = plain.bar.colorMap() + assert cmap.map([0.0], mode=cmap.FLOAT)[0][2] > 0.9 # bwr starts blue def test_plots_map_says_a_message_in_place_of_the_image(qtbot): @@ -221,8 +237,8 @@ def test_plots_log_axis_drops_non_positive_values(qtbot): plot.update_curve(0, x, np.array([1.0, 0.0, 1.0, 1.0, 1.0])) assert plot.items[0].yData.size == 4 plot.fit() - # log10 of 1 … 1: a flat line gets a unit-wide window around it. - assert plot.box.viewRange()[1] == pytest.approx([-0.05, 0.05]) + # A log axis spans whole decades (at least one), so it always has ticks. + assert plot.box.viewRange()[1] == pytest.approx([0.0, 1.0]) def test_plots_right_axis_carries_its_own_curves(qtbot): @@ -246,6 +262,22 @@ def test_plots_right_range_is_ignored_without_a_right_axis(qtbot): assert plot.right_box is None +def test_plots_right_label_needs_a_right_axis(qtbot): + with_right = _shown(qtbot, LinePlot(right_label="Phase")) + with_right.set_right_label("Phase (rad)") + assert with_right.item.getAxis("right").labelText == "Phase (rad)" + without = _shown(qtbot, LinePlot()) + without.set_right_label("ignored") # no right axis: nothing to label + + +def test_plots_curves_can_be_thin_references_drawn_beneath(qtbot): + plot = _shown(qtbot, LinePlot()) + x = np.arange(3.0) + plot.set_curves([Curve(x, x), Curve(x, x, width=0.9, under=True)]) + assert plot.items[1].zValue() < plot.items[0].zValue() + assert plot.items[1].opts["pen"].widthF() == pytest.approx(0.9) + + def test_plots_marks_replace_the_previous_set(qtbot): plot = _shown(qtbot, LinePlot()) plot.set_marks([1.0, 2.0]) diff --git a/tests/gui/test_retrieval_views.py b/tests/gui/test_retrieval_views.py new file mode 100644 index 0000000..c558ee9 --- /dev/null +++ b/tests/gui/test_retrieval_views.py @@ -0,0 +1,173 @@ +"""The retrieval's panels on screen: the live overview and Analyse's result pages.""" + +from __future__ import annotations + +from dataclasses import replace + +import numpy as np +import pytest + +pytest.importorskip("PySide6") + +from PySide6.QtWidgets import QFileDialog + +from croak import plotting +from croak.forward import maketrace +from croak.grid import Grid +from croak.gui.retrieval_views import ( + OVERVIEW, + PANELS, + ResultPage, + RetrievalPanels, +) +from croak.gui.state import WizardState +from croak.maths import wlfreq +from croak.processing import TruthPulse +from croak.pulses import gaussian_pulse +from croak.retrieve import retrieve + + +@pytest.fixture(scope="module") +def data(): + """A short PG retrieval with a measured spectrum and a known truth.""" + g = Grid(96, dt=0.5e-15) + omega0 = float(wlfreq(800e-9)) + ew = gaussian_pulse(g, 7e-15, phases=[20e-30]) + delays = np.linspace(-50e-15, 50e-15, 60) + trace = maketrace(g.omega, delays, ew, "pg") + res = retrieve( + trace, + g.omega, + delays, + "pg", + algorithm="copra", + maxiters=20, + guess=ew, + omega0=omega0, + ) + return plotting.retrieval_plot_data( + res, + measured=trace, + Iomega_meas=np.abs(ew) ** 2, + lam_min=700e-9, + lam_max=950e-9, + truth=TruthPulse.from_spectrum(g, ew, omega0), + ) + + +def test_retrieval_views_cover_every_figure_panel(): + assert set(PANELS) == set(plotting.RETRIEVAL_PANELS) + + +def test_retrieval_panels_move_from_progress_to_results(qtbot, data): + panels = RetrievalPanels() + qtbot.addWidget(panels) + assert panels.mode == "empty" + panels.show_progress([0.5, 0.2, 0.1]) + assert panels.mode == "progress" + assert panels.convergence.log_y + panels.set_data(data) + assert panels.mode == "panels" and panels.data is data + assert set(panels.boxes) == {k for row in OVERVIEW for k in row} + temporal = panels.boxes["temporal"] + assert temporal.title.text() == "Retrieved pulse" + assert temporal.plot.right_box is not None # the phase axis + spectral = panels.boxes["spectral"] + assert spectral.notes.text().startswith("GDD:") + assert spectral.notes.property("warn") is False + panels.clear() + assert panels.mode == "empty" and panels.data is None + + +def test_retrieval_panels_draw_every_page(qtbot, data): + for spec in plotting.RETRIEVAL_PAGES.values(): + panels = RetrievalPanels(spec.mosaic) + qtbot.addWidget(panels) + panels.set_data(data) + for key, box in panels.boxes.items(): + assert box.title.text(), key + traces = RetrievalPanels(plotting.RETRIEVAL_PAGES["traces"].mosaic) + qtbot.addWidget(traces) + traces.resize(900, 700) + traces.show() + qtbot.waitExposed(traces) + traces.set_data(data) + note = traces.boxes["retrieved_lin"].notes.text() + assert note.startswith("R ") and "×" in note + # The trace maps are linked: zooming one zooms the others. + first = traces.boxes["measured_log"].plot + first.box.setXRange(-10, 10, padding=0) + other = traces.boxes["retrieved_lin"].plot + assert other.box.viewRange()[0] == pytest.approx([-10, 10]) + + +def test_retrieval_panels_say_when_there_is_no_residual(qtbot, data): + panels = RetrievalPanels((("residual",),)) + qtbot.addWidget(panels) + panels.set_data(replace(data, processed=replace(data.processed, residual=None))) + box = panels.boxes["residual"] + assert box.title.text() == "Residuals" + assert box.plot.data is None and box.plot.message.isVisible() + + +def test_retrieval_panels_warn_about_edge_energy(qtbot, data): + panels = RetrievalPanels((("spectral",),)) + qtbot.addWidget(panels) + leaky = replace(data, processed=replace(data.processed, edge_energy=0.5)) + panels.set_data(leaky) + assert panels.boxes["spectral"].notes.property("warn") is True + + +def test_result_page_follows_the_result_and_pops_out(qtbot, data): + state = WizardState() + current = {"data": None} + page = ResultPage(state, "pulse", lambda: current["data"]) + qtbot.addWidget(page) + page.show() + page.on_enter() + assert page.panels.mode == "empty" + current["data"] = data + state.result_changed.emit() # visible: redrawn at once + assert page.panels.mode == "panels" + dialog = page.pop_out() + follower = dialog.findChild(RetrievalPanels) + assert follower is not None and follower.mode == "panels" + current["data"] = replace(data) + state.result_changed.emit() + assert follower.data is current["data"] + dialog.close() + assert page.popouts == [] + page.hide() + state.result_changed.emit() # hidden: drawn when next shown + empty = ResultPage(state, "overview", lambda: None) + qtbot.addWidget(empty) + assert empty.pop_out().findChild(RetrievalPanels).mode == "empty" + + +def test_result_page_exports_its_figure(qtbot, data, tmp_path, monkeypatch): + state = WizardState() + messages = [] + page = ResultPage(state, "overview", lambda: data) + page.message.connect(messages.append) + qtbot.addWidget(page) + monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: ("", "")) + assert page.export_figure() == "" # cancelled + target = tmp_path / "overview.png" + monkeypatch.setattr( + QFileDialog, "getSaveFileName", lambda *a, **k: (str(target), "") + ) + assert page.export_figure() == str(target) + assert target.stat().st_size > 0 + assert messages == ["Saved overview.png"] + diagnostics = ResultPage(state, "diagnostics", lambda: data) + qtbot.addWidget(diagnostics) + target = tmp_path / "diagnostics.svg" + monkeypatch.setattr( + QFileDialog, "getSaveFileName", lambda *a, **k: (str(target), "") + ) + assert diagnostics.export_figure() == str(target) + nothing = ResultPage(state, "traces", lambda: None) + nothing.message.connect(messages.append) + qtbot.addWidget(nothing) + assert nothing.export_figure() == "" + assert "Run a retrieval first" in messages[-1] diff --git a/tests/gui/test_wizard_flow.py b/tests/gui/test_wizard_flow.py index 48ed156..c7ce078 100644 --- a/tests/gui/test_wizard_flow.py +++ b/tests/gui/test_wizard_flow.py @@ -2484,16 +2484,7 @@ def test_collapsible_strip_folds_and_remembers_its_state(qtbot): assert plain.isExpanded() # no key: the default -# -- Retrieve pages ---------------------------------------------------------------- -def _titles(figure): - return [ax.get_title() for ax in figure.axes if ax.get_title()] - - -def _page_titles(key): - page = croak.plotting.RETRIEVAL_PAGES[key] - return [croak.plotting.RETRIEVAL_PANELS[k].title for k in page.keys] - - +# -- Retrieve's live overview and Analyse's pages --------------------------------- class _IdleWorker: """A retrieval worker that never runs, so a just-started state can be inspected.""" @@ -2509,78 +2500,68 @@ def start(self): pass -def test_retrieve_pages_show_three_tabs_and_draw_the_visible_page(qtbot): - w = Driver(qtbot) - rs = _run_synthetic_retrieval(w, qtbot) - pages = rs.pages - assert pages.page_keys() == ("traces", "pulse", "diagnostics") - assert pages.current_page() == "traces" - assert _titles(pages.canvas("traces").figure) == _page_titles("traces") - assert pages.is_stale("pulse") and pages.is_stale("diagnostics") - assert not pages.canvas("pulse").figure.axes # not drawn until selected - - -def test_retrieve_pages_redraw_a_stale_page_when_selected(qtbot): +def test_retrieve_shows_the_overview_after_a_run(qtbot): w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) - rs.pages.set_current_page("pulse") - assert not rs.pages.is_stale("pulse") - assert _titles(rs.pages.canvas("pulse").figure) == _page_titles("pulse") - rs.pages.set_current_page("diagnostics") - assert _titles(rs.pages.canvas("diagnostics").figure) == _page_titles("diagnostics") + assert rs.pages.mode == "panels" + assert set(rs.pages.boxes) == { + "measured_log", + "retrieved_log", + "temporal", + "spectral", + } + assert rs.pages.data is not None and rs.pages.data.result is w.state.result -def test_retrieve_pages_pop_out_opens_a_window_that_follows_the_result(qtbot): +def test_analyse_rail_shows_every_page_of_the_result(qtbot): w = Driver(qtbot) - rs = _run_synthetic_retrieval(w, qtbot) - dlg = rs.pages.pop_out("pulse") - assert dlg.isWindow() - assert dlg.isVisible() - assert _titles(dlg.canvas.figure) == _page_titles("pulse") - before = dlg.canvas.figure.axes[0] - rs._update_view() # the pop-out follows, updated in place - assert dlg.canvas.figure.axes[0] is before - assert rs.pages.pop_out("pulse") is dlg # one window per page - dlg.close() - qtbot.waitUntil(lambda: "pulse" not in rs.pages._popouts) + _run_synthetic_retrieval(w, qtbot) + w.win.go(Stage.ANALYSE) + analyse = w.analyse + assert list(analyse.rail_items)[:4] == [ + "overview", + "traces", + "pulse", + "diagnostics", + ] + for key in ("traces", "pulse", "diagnostics"): + analyse.show_page(key) + page = analyse.pages[key] + expected = set(croak.plotting.RETRIEVAL_PAGES[key].keys) + assert set(page.panels.boxes) == expected + assert page.panels.mode == "panels" -def test_retrieve_progress_curve_shows_on_the_visible_page(qtbot): +def test_retrieve_progress_curve_shows_before_the_first_preview(qtbot): w = Driver(qtbot) w.start("synthetic") w.synthetic.advance() w.goto(3) w.goto(4) rs = w.stages[3] - rs.pages.set_current_page("pulse") rs._errors = [] rs._on_progress(1, 0.1, 0.1) rs._on_progress(2, 0.05, 0.05) - fig = rs.pages.canvas("pulse").figure - qtbot.waitUntil(lambda: _titles(fig) == ["Convergence"]) # after the debounce - line = fig.axes[0].lines[0] - assert line.get_xydata().shape == (2, 2) + qtbot.waitUntil(lambda: rs.pages.mode == "progress") # after the debounce + line = rs.pages.convergence.items[0] + assert line.yData.size == 2 rs._on_progress(3, 0.02, 0.02) - qtbot.waitUntil(lambda: line.get_xydata().shape == (3, 2)) # grown in place - assert fig.axes[0].lines[0] is line - # switching tabs while running shows the curve there too - rs.pages.set_current_page("traces") - assert _titles(rs.pages.canvas("traces").figure) == ["Convergence"] + qtbot.waitUntil(lambda: rs.pages.convergence.items[0].yData.size == 3) -def test_retrieve_run_clears_the_pages(qtbot, monkeypatch): +def test_retrieve_run_clears_the_overview(qtbot, monkeypatch): from croak.gui import stage_retrieve as sr w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) - assert rs.pages.canvas("traces").figure.axes + assert rs.pages.mode == "panels" monkeypatch.setattr(sr, "RetrievalWorker", _IdleWorker) rs._run() - assert all(not rs.pages.canvas(k).figure.axes for k in rs.pages.page_keys()) + assert rs.pages.mode == "empty" assert rs.pages.data is None -def test_retrieve_readout_collapse_frees_canvas_height(qtbot): +def test_retrieve_readout_collapse_frees_plot_height(qtbot): w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) w.resize(1366, 768) @@ -2616,12 +2597,11 @@ def spy(data, page, **kwargs): assert (tmp_path / "retrieval.pdf").stat().st_size > 0 page, fig = seen[-1] assert page is croak.plotting.RETRIEVAL_OVERVIEW - assert len(_titles(fig)) == 12 + assert len([ax for ax in fig.axes if ax.get_title()]) == 12 -# -- live preview in place ----------------------------------------------------------- -def test_retrieve_live_preview_updates_the_page_in_place(qtbot): - """A later snapshot of the same run reuses the drawn axes and artists.""" +def test_retrieve_live_preview_redraws_the_overview(qtbot): + """A later snapshot of the same run is drawn on the same plots.""" from dataclasses import replace w = Driver(qtbot) @@ -2630,33 +2610,21 @@ def test_retrieve_live_preview_updates_the_page_in_place(qtbot): assert r1 is not None r2 = replace(r1, errors=list(r1.errors[:5]), error=float(r1.errors[4])) rs._on_preview(r1) - figure = rs.pages.canvas("traces").figure - axes = list(figure.axes) - retrieved_lin = next( - ax for ax in figure.axes if ax.get_title() == "Retrieved (lin)" - ) - label = retrieved_lin.texts[0] # the bold error/shape annotation + plot = rs.pages.boxes["temporal"].plot rs._on_preview(r2) - assert list(figure.axes) == axes + assert rs.pages.boxes["temporal"].plot is plot assert rs._live_full - assert f"{r2.error * 100:.2f}%" in label.get_text() + assert rs.pages.data is not None and rs.pages.data.result is r2 assert "iter 5" in rs.status_label.text() - # a stale page drawn later shows the latest snapshot - rs.pages.set_current_page("pulse") - assert rs.pages.data is not None - assert rs.pages.data.result is r2 -def test_retrieve_post_filter_updates_in_place(qtbot): +def test_retrieve_post_filter_redraws_the_stored_result(qtbot): w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) - figure = rs.pages.canvas("traces").figure - axes = list(figure.axes) - status = rs.status_label.text() + before = rs.pages.data rs.pf_lam.setChecked(True) # re-plots the stored result through _update_view - assert list(figure.axes) == axes assert rs.state.retrieve.post_filter_lam is True - assert rs.status_label.text() != status or "Done" in rs.status_label.text() + assert rs.pages.data is not before def test_retrieve_convergence_redraw_is_throttled(qtbot, monkeypatch): @@ -2682,22 +2650,14 @@ def test_retrieve_convergence_redraw_is_throttled(qtbot, monkeypatch): # -- spectrum axis and preview cadence ------------------------------------------------- -def _spectrum_axes(rs): - return next( - ax - for ax in rs.pages.canvas("pulse").figure.axes - if ax.get_title() == "Spectrum" - ) - - def test_retrieve_spectrum_axis_defaults_to_frequency_and_is_remembered(qtbot): w = Driver(qtbot) rs = _run_synthetic_retrieval(w, qtbot) assert rs.spectrum_axis_combo.currentText() == "frequency" - rs.pages.set_current_page("pulse") - assert _spectrum_axes(rs).get_xlabel() == "Frequency (PHz)" + spectrum = rs.pages.boxes["spectral"].plot + assert spectrum.item.getAxis("bottom").labelText == "Frequency (PHz)" rs.spectrum_axis_combo.setCurrentText("wavelength") # re-presents the result - assert _spectrum_axes(rs).get_xlabel() == "Wavelength (nm)" + assert spectrum.item.getAxis("bottom").labelText == "Wavelength (nm)" assert settings.text("retrieve/spectrum_axis", "frequency") == "wavelength" again = Driver(qtbot) assert again.stages[3].spectrum_axis_combo.currentText() == "wavelength" diff --git a/tests/test_plotting_screen.py b/tests/test_plotting_screen.py new file mode 100644 index 0000000..01537cb --- /dev/null +++ b/tests/test_plotting_screen.py @@ -0,0 +1,162 @@ +"""The screen data of the interactive retrieval views (:mod:`croak.plotting.screen`).""" + +from __future__ import annotations + +from dataclasses import replace + +import numpy as np +import pytest +from numpy.testing import assert_allclose + +from croak import plotting +from croak.forward import maketrace +from croak.grid import Grid +from croak.maths import wlfreq +from croak.plotting import screen +from croak.processing import TruthPulse +from croak.pulses import gaussian_pulse +from croak.retrieve import retrieve + + +@pytest.fixture(scope="module") +def data(): + """A short PG retrieval with a known truth, as the panels' plot data.""" + g = Grid(96, dt=0.5e-15) + omega0 = float(wlfreq(800e-9)) + ew = gaussian_pulse(g, 7e-15, phases=[20e-30]) + delays = np.linspace(-50e-15, 50e-15, 60) + trace = maketrace(g.omega, delays, ew, "pg") + res = retrieve( + trace, + g.omega, + delays, + "pg", + algorithm="copra", + maxiters=20, + guess=ew, + omega0=omega0, + ) + truth = TruthPulse.from_spectrum(g, ew, omega0) + return plotting.retrieval_plot_data( + res, + measured=trace, + Iomega_meas=np.abs(ew) ** 2, + lam_min=700e-9, + lam_max=950e-9, + truth=truth, + flim=(0.7e15, 0.8e15), + ) + + +def test_screen_uniform_image_reverses_and_resamples(): + x, y, values = screen.uniform_image( + [0.0, 1.0, 3.0], [1.0, 0.0], [[0.0, 1.0, 3.0], [0.0, 2.0, 6.0]] + ) + assert_allclose(x, [0.0, 1.5, 3.0]) + assert_allclose(y, [0.0, 1.0]) + # Linear in x along each row: resampling moves no feature. + assert_allclose(values, [[0.0, 3.0, 6.0], [0.0, 1.5, 3.0]]) + + +@pytest.mark.parametrize( + ("x", "y", "values", "match"), + [ + ([0.0, 1.0], [0.0, 1.0], [[1.0, 2.0]], "do not match"), + ([0.0], [0.0], [[1.0]], "at least two samples"), + ], +) +def test_screen_uniform_image_rejects_bad_shapes(x, y, values, match): + with pytest.raises(ValueError, match=match): + screen.uniform_image(x, y, values) + + +def test_screen_log_trace_spans_the_dynamic_range(data): + view = screen.trace_map(data, "measured", db=True) + assert view.levels == (-data.tracedb, 0.0) and view.unit == "dB" + assert view.values.max() == pytest.approx(0.0) + assert view.values.min() >= -data.tracedb + assert view.title == "Measured (log)" and not view.signed + assert view.yrange == pytest.approx((0.7, 0.8)) # flim in PHz + assert view.xrange == pytest.approx((-50.0, 50.0)) + + +def test_screen_linear_trace_shares_its_scale_and_shows_negatives(data): + view = screen.trace_map(data, "retrieved", db=False) + assert view.levels == (0.0, data.lin_vmax) and view.signed + dipped = replace(data, measured=data.measured - 0.1 * data.measured.max()) + signed = screen.trace_map(dipped, "measured", db=False) + assert signed.levels[0] < 0 < signed.levels[1] + assert ( + screen.trace_map(replace(data, flim=None), "measured", db=True).yrange is None + ) + with pytest.raises(ValueError, match="'measured' or 'retrieved'"): + screen.trace_map(data, "truth", db=True) + + +def test_screen_residual_is_symmetric_and_optional(data): + view = screen.residual_map(data) + assert view is not None + low, high = view.levels + assert low == pytest.approx(-high) and view.colours == "bwr" + empty = replace(data, processed=replace(data.processed, residual=None)) + assert screen.residual_map(empty) is None + + +def test_screen_spectrogram_is_in_the_wavelength_window(data): + view = screen.spectrogram_map(data) + assert view.yrange == pytest.approx((700.0, 950.0)) + assert view.levels[0] == 0.0 and view.levels[1] > 0 + + +def test_screen_temporal_view_has_pulse_limit_truth_and_phases(data): + view = screen.temporal_view(data) + roles = [c.role for c in view.curves] + assert roles == ["retrieved", "transform-limited", "truth", "phase", "truth-phase"] + assert [c.right for c in view.curves] == [False, False, False, True, True] + assert view.yrange is not None and view.yrange[0] == 0.0 + assert view.right_range is not None + assert view.curves[2].name.startswith("truth (") + + +def test_screen_spectral_view_has_notes_and_warns_on_edge_energy(data): + view = screen.spectral_view(data) + roles = [c.role for c in view.curves] + assert roles[:3] == ["retrieved", "measured", "truth"] + assert "fit" in roles and "truth-phase" in roles + (dispersion, _), (edge, warn) = view.notes + assert dispersion.startswith("GDD:") and edge.startswith("Edge:") + assert not warn + leaky = replace(data, processed=replace(data.processed, edge_energy=0.5)) + assert screen.spectral_view(leaky).notes[1][1] is True + + +def test_screen_spectral_view_without_measurement_or_truth(data): + bare = replace( + data, + truth=None, + processed=replace(data.processed, Iw_meas=None), + spectral_axis="wavelength", + ) + view = screen.spectral_view(bare) + assert [c.role for c in view.curves] == ["retrieved", "phase", "fit"] + assert view.xlabel == "Wavelength (nm)" + assert screen.temporal_view(bare).curves[-1].role == "phase" + + +def test_screen_convergence_view_marks_inner_handovers_only(): + view = screen.convergence_view([1.0, 0.5, 0.2, 0.1], boundaries=[0, 2, 4]) + assert view.log_y and view.marks == (2.5,) + assert_allclose(view.curves[0].x, [1, 2, 3, 4]) + + +def test_screen_marginals_and_filter(data): + freq = screen.frequency_marginal_view(data) + delay = screen.delay_marginal_view(data) + assert [c.role for c in freq.curves] == ["retrieved", "measured"] + assert delay.xrange == pytest.approx((-50.0, 50.0)) + no_meas = replace(data, processed=replace(data.processed, tau_marg_meas=None)) + assert len(screen.delay_marginal_view(no_meas).curves) == 1 + filt = screen.filter_view(data) + x = filt.curves[0].x + assert np.all(np.diff(x) > 0) # sorted by wavelength + assert filt.xrange == pytest.approx((700.0, 950.0)) From 68948c7bb4cf6799d686521200df2cbf768f1891 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 22:41:03 +0000 Subject: [PATCH 09/41] Draw Load and the marginal check on pyqtgraph A loaded trace is a signed map scaled by its trusted band (screen.raw_trace_map); the marginal comparison is a line view with the predicted and corrected marginals and coloured centroid rules. Line views gain roles for those curves and rules with a role each, and a log axis honours a view's fixed window in decades. Co-Authored-By: Claude Opus 5.5 --- croak/gui/plots.py | 11 +- croak/gui/retrieval_views.py | 107 ++++++++++++----- croak/gui/stage_load.py | 43 ++----- croak/gui/stage_marginal_check.py | 158 ++++++++++--------------- croak/plotting/screen.py | 68 ++++++++++- tests/gui/test_plots.py | 4 +- tests/gui/test_stage_load.py | 21 ++-- tests/gui/test_stage_marginal_check.py | 33 +++--- tests/gui/test_wizard_flow.py | 28 +---- tests/test_plotting_screen.py | 17 ++- 10 files changed, 264 insertions(+), 226 deletions(-) diff --git a/croak/gui/plots.py b/croak/gui/plots.py index 44cadfb..19c057a 100644 --- a/croak/gui/plots.py +++ b/croak/gui/plots.py @@ -538,13 +538,16 @@ def set_right_range(self, low: float, high: float) -> None: if self.right_box is not None: self.right_box.setYRange(low, high, padding=0) - def set_marks(self, positions: Sequence[float], colour: str = "") -> None: - """Draw dashed vertical lines at ``positions`` (replacing the last set).""" + def set_marks( + self, marks: Sequence[tuple[float, str]], *, dashed: bool = True + ) -> None: + """Draw vertical rules, each ``(x, colour)`` (replacing the last set).""" for mark in self.marks: self.item.removeItem(mark) - pen = pg.mkPen(colour or PALETTE.accent, width=1.2, style=Qt.PenStyle.DashLine) + style = Qt.PenStyle.DashLine if dashed else Qt.PenStyle.DotLine self.marks = [] - for position in positions: + for position, colour in marks: + pen = pg.mkPen(colour or PALETTE.accent, width=1.2, style=style) line = pg.InfiniteLine(pos=position, angle=90, pen=pen) self.addItem(line, ignoreBounds=True) # pyright: ignore[reportCallIssue] self.marks.append(line) diff --git a/croak/gui/retrieval_views.py b/croak/gui/retrieval_views.py index a73aa0f..459ca61 100644 --- a/croak/gui/retrieval_views.py +++ b/croak/gui/retrieval_views.py @@ -50,6 +50,7 @@ __all__ = [ "OVERVIEW", + "ViewPanel", "PANELS", "Panel", "ResultPage", @@ -73,6 +74,10 @@ "fit": (COLOURS[3], True, False), "truth-phase": (PALETTE.muted, True, True), "error": (COLOURS[0], False, False), + "predicted": (COLOURS[4], True, False), + "corrected": (COLOURS[0], True, False), + "faint": ("#9cbfdd", False, False), + "boundary": (PALETTE.muted, True, False), } REFERENCE_WIDTH = 0.9 """The width of a reference curve (the known pulse), logical px.""" @@ -138,11 +143,13 @@ def draw_line_view(plot: LinePlot, view: sc.LineView) -> None: if view.xrange is not None or view.yrange is not None: (x0, x1), (y0, y1) = plot.box.viewRange() xr = view.xrange or (x0, x1) - yr = view.yrange if view.yrange is not None and not view.log_y else (y0, y1) + yr = view.yrange or (y0, y1) + if view.log_y and view.yrange is not None: # a log axis works in decades + yr = (float(np.log10(yr[0])), float(np.log10(yr[1]))) plot.set_view(xr, yr) if view.right_range is not None: plot.set_right_range(*view.right_range) - plot.set_marks(view.marks, PALETTE.muted) + plot.set_marks([(x, ROLE_STYLE[role][0]) for x, role in view.marks]) def draw_map_view(plot: MapPlot, view: sc.MapView) -> None: @@ -153,13 +160,36 @@ def draw_map_view(plot: MapPlot, view: sc.MapView) -> None: plot.set_view(view.xrange or (x0, x1), view.yrange or (y0, y1)) -class PanelBox(QWidget): - """A titled panel: its plot, with a line of notes under the title.""" +def _name(axis_label: str) -> str: + """Return an axis label's quantity for the readout: "Delay (fs)" → "Delay".""" + return axis_label.split(" (")[0] - def __init__(self, key: str) -> None: + +class ViewPanel(QWidget): + """A titled plot that shows a screen view (:mod:`croak.plotting.screen`). + + Parameters + ---------- + kind : {"map", "line"} + The plot it holds. + colours : str + A map's colour map. + signed, white : bool + A map's colour scale (see :class:`~croak.gui.plots.MapPlot`). + right : bool + A line plot with a right-hand axis. + """ + + def __init__( + self, + kind: str, + *, + colours: str = "viridis", + signed: bool = False, + white: bool = True, + right: bool = False, + ) -> None: super().__init__() - self.key = key - self.spec = PANELS[key] layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(2) @@ -172,40 +202,59 @@ def __init__(self, key: str) -> None: head.addWidget(self.notes) layout.addLayout(head) self.plot: MapPlot | LinePlot - if self.spec.kind == "map": - plot = MapPlot( - colours=self.spec.colours, - signed=self.spec.signed, - white=self.spec.white, - ) - plot.reader = map_reader(plot, "x", "y") - self.plot = plot + if kind == "map": + self.plot = MapPlot(colours=colours, signed=signed, white=white) else: - line = LinePlot(right_label="" if self.spec.right else None) - line.reader = curve_reader(line, "x") - self.plot = line + self.plot = LinePlot(right_label="" if right else None) layout.addWidget(self.plot, stretch=1) - def show_data(self, data: RetrievalPlotData) -> None: - """Draw this panel's view of ``data``.""" - view = self.spec.view(data) - if view is None: # only the residual can be missing - self.title.setText("Residuals") - cast(MapPlot, self.plot).say("No residual for this result.") - self.notes.setText("") - return + def show_view(self, view: sc.MapView | sc.LineView, notes: str = "") -> None: + """Draw ``view``; ``notes`` (or a line view's own) go beside the title.""" self.title.setText(view.title) if isinstance(view, sc.MapView): - draw_map_view(cast(MapPlot, self.plot), view) - notes = self.spec.note(data) if self.spec.note is not None else "" + plot = cast(MapPlot, self.plot) + draw_map_view(plot, view) + plot.reader = map_reader(plot, _name(view.xlabel), _name(view.ylabel)) self.notes.setText(notes) set_flag(self.notes, "warn", False) return line = cast(LinePlot, self.plot) draw_line_view(line, view) - self.notes.setText(" ".join(text for text, _ in view.notes)) + line.reader = curve_reader(line, _name(view.xlabel)) + self.notes.setText(notes or " ".join(text for text, _ in view.notes)) set_flag(self.notes, "warn", any(warn for _, warn in view.notes)) + def say(self, title: str, text: str) -> None: + """Show ``text`` in place of a map (there is nothing to draw).""" + self.title.setText(title) + cast(MapPlot, self.plot).say(text) + self.notes.setText("") + + +class PanelBox(ViewPanel): + """One of the retrieval's panels (:data:`PANELS`).""" + + def __init__(self, key: str) -> None: + spec = PANELS[key] + super().__init__( + spec.kind, + colours=spec.colours, + signed=spec.signed, + white=spec.white, + right=spec.right, + ) + self.key = key + self.spec = spec + + def show_data(self, data: RetrievalPlotData) -> None: + """Draw this panel's view of ``data``.""" + view = self.spec.view(data) + if view is None: # only the residual can be missing + self.say("Residuals", "No residual for this result.") + return + note = self.spec.note(data) if self.spec.note is not None else "" + self.show_view(view, note) + class RetrievalPanels(QWidget): """A grid of retrieval panels, with a convergence view for a run's start. diff --git a/croak/gui/stage_load.py b/croak/gui/stage_load.py index fe9385e..8984e1f 100644 --- a/croak/gui/stage_load.py +++ b/croak/gui/stage_load.py @@ -24,10 +24,11 @@ from .. import io from ..interactions import get_interaction -from ..plotting import cmap_white, signed_pcolormesh +from ..plotting.screen import raw_trace_map from ..session.pipeline import assemble_load_data from .base import Stage -from .canvas import MplCanvas, check, combo, group, with_toolbar +from .canvas import check, combo, group +from .retrieval_views import ViewPanel from .state import PreprocParams from .widgets import SciSpinBox @@ -456,8 +457,8 @@ def __init__(self, state): self.controls.addWidget(load_btn) self.controls.addStretch(1) - self.trace_canvas = MplCanvas() - self.set_plot_area(with_toolbar(self.trace_canvas)) + self.trace_view = ViewPanel("map", signed=True) + self.set_plot_area(self.trace_view) self.apply_help() @@ -798,32 +799,10 @@ def _draw(self, data): tau_fs = ( io.position_to_delay(scan) if data["input_unit"] == "position" else scan ) / 1e-15 - trace = data["trace"] - lo, hi = _PREVIEW_TRUST_NM - in_band = (lam_nm >= lo) & (lam_nm <= hi) - band = trace[in_band] if np.any(in_band) else trace - vmax = float(np.nanmax(np.abs(band))) if band.size else 0.0 - self.trace_canvas.render( - lambda fig: self._plot_trace( - fig.add_subplot(111), tau_fs, lam_nm, trace, in_band, vmax - ) - ) - - @staticmethod - def _plot_trace(ax, tau_fs, lam_nm, trace, in_band, vmax) -> None: - """Draw the raw-trace preview into ``ax`` (the body of :meth:`_draw`).""" - signed_pcolormesh( - ax, - tau_fs, - lam_nm, - trace, - db=False, - cmap_pos=cmap_white("viridis"), - vmax=vmax if vmax > 0 else None, + trace = np.asarray(data["trace"]) + if min(trace.shape) < 2: # nothing to draw as an image + self.trace_view.say("Measured trace", "The trace holds no samples.") + return + self.trace_view.show_view( + raw_trace_map(tau_fs, lam_nm, trace, _PREVIEW_TRUST_NM) ) - sig = lam_nm[in_band & (trace.max(axis=1) > 1e-2 * (vmax or 1.0))] - if sig.size: - ax.set_ylim(sig.min(), sig.max()) - ax.set_xlabel("Delay (fs)") - ax.set_ylabel("Wavelength (nm)") - ax.set_title("Measured trace") diff --git a/croak/gui/stage_marginal_check.py b/croak/gui/stage_marginal_check.py index 89b5e3b..61bf665 100644 --- a/croak/gui/stage_marginal_check.py +++ b/croak/gui/stage_marginal_check.py @@ -38,7 +38,7 @@ rms_width, ) from ..maths import planck_taper, wlfreq -from ..plotting import cmap_white, signed_pcolormesh +from ..plotting.screen import CurveView, LineView, raw_trace_map from ..preprocess import ( omega_to_lambda_density, resample_spectrum, @@ -46,7 +46,8 @@ ) from ..session.pipeline import assemble_load_data, assemble_simulated_load_data from .base import Stage -from .canvas import MplCanvas, add_wide_row, check, combo, dspin, group, with_toolbar +from .canvas import add_wide_row, check, combo, dspin, group +from .retrieval_views import ViewPanel from .widgets import RangeControl # Number of uniform angular-frequency samples used to resample the independent @@ -215,11 +216,11 @@ def __init__(self, state): # Trace above, marginal comparison below, on a draggable divider so a # short display can give either plot the height (equal split by default). - self.trace_canvas = MplCanvas() - self.marg_canvas = MplCanvas() + self.trace_view = ViewPanel("map", signed=True) + self.marg_view = ViewPanel("line") self.plot_splitter = QSplitter(Qt.Orientation.Vertical) - self.plot_splitter.addWidget(with_toolbar(self.trace_canvas)) - self.plot_splitter.addWidget(with_toolbar(self.marg_canvas)) + self.plot_splitter.addWidget(self.trace_view) + self.plot_splitter.addWidget(self.marg_view) self.plot_splitter.setStretchFactor(0, 1) self.plot_splitter.setStretchFactor(1, 1) self.plot_splitter.setChildrenCollapsible(True) @@ -430,47 +431,16 @@ def _draw(self) -> None: self._draw_marginal(lam, lam_nm, trace, in_band, data) def _draw_trace(self, tau_fs, lam_nm, trace, in_band) -> None: - """Trace image with the preprocess-page colour scheme and trust-scaled vmax.""" - self.trace_canvas.render( - lambda fig: self._plot_trace( - fig.add_subplot(111), tau_fs, lam_nm, trace, in_band - ) - ) - - @staticmethod - def _plot_trace(ax, tau_fs, lam_nm, trace, in_band) -> None: - """Draw the trace image into ``ax`` (the body of :meth:`_draw_trace`).""" - # Scale to the in-trust-region peak so amplified out-of-band noise (the - # (λ/µm)^exp tilt blows up the long-λ tail) cannot wash out the signal. - band = trace[in_band] if np.any(in_band) else trace - vmax = float(np.nanmax(np.abs(band))) if band.size else 1.0 - signed_pcolormesh( - ax, - tau_fs, - lam_nm, - trace, - db=False, - cmap_pos=cmap_white("viridis"), - vmax=vmax if vmax > 0 else None, - ) - # Clip the wavelength axis to the in-band signal so the view tracks it. - sig = lam_nm[in_band & (trace.max(axis=1) > 1e-2 * (vmax or 1.0))] - if sig.size: - ax.set_ylim(sig.min(), sig.max()) - ax.set_xlabel("Delay (fs)") - ax.set_ylabel("Wavelength (nm)") - ax.set_title("Measured trace") + """Trace image, scaled by the trust region (see ``raw_trace_map``).""" + del in_band # the map recomputes the band from the trust region + self.trace_view.show_view(raw_trace_map(tau_fs, lam_nm, trace, self._trust_nm)) def _draw_marginal(self, lam, lam_nm, trace, in_band, data) -> None: - """Trace marginal against the spectrum (see :meth:`_plot_marginal`).""" - self.marg_canvas.render( - lambda fig: self._plot_marginal( - fig.add_subplot(111), lam, lam_nm, trace, in_band, data - ) - ) + """Draw the trace marginal against the spectrum and the prediction.""" + self.marg_view.show_view(self._marginal_view(lam, lam_nm, trace, in_band, data)) - def _plot_marginal(self, ax, lam, lam_nm, trace, in_band, data) -> None: - """Draw the marginal comparison into ``ax``; reads the display combos live.""" + def _marginal_view(self, lam, lam_nm, trace, in_band, data) -> LineView: + """Build the marginal comparison; reads the display combos live.""" lo, hi = self._trust_nm # nm log = self.scale_combo.currentText() == "log" freq = self.axis_combo.currentText() == "frequency" @@ -479,9 +449,9 @@ def _plot_marginal(self, ax, lam, lam_nm, trace, in_band, data) -> None: # ω-density vs ν=ω/2π (undistorted). On the wavelength axis we plot the # λ-density vs λ; the λ Jacobian (×1/λ²) then blue-shifts a broad marginal # even when its frequency centroid matches — see the 'marginal axis' help. - # When the SHG marginal correction is active, dim the raw marginal and - # overlay the corrected one (which, by construction, equals the predicted - # SHG autoconvolution — see _draw the dashed C0 curve below). + # When the SHG marginal correction is active, the raw marginal is drawn + # faint and the corrected one (which, by construction, equals the + # predicted SHG autoconvolution) is overlaid in the trace colour, dashed. correcting = self.marginal_correct_check.isChecked() and ( self._marginal_correct_available(data) ) @@ -490,14 +460,14 @@ def _plot_marginal(self, ax, lam, lam_nm, trace, in_band, data) -> None: x_meas, y_meas = wlfreq(lam) / _TWO_PI_PHZ, marg * lam**2 else: x_meas, y_meas = lam_nm, marg - ax.plot( - x_meas, - _peak_norm(y_meas, in_band), - label="trace marginal", - c="C0", - alpha=0.45 if correcting else 1.0, - ) - + curves = [ + CurveView( + np.asarray(x_meas, float), + _peak_norm(y_meas, in_band), + "faint" if correcting else "retrieved", + "trace marginal", + ) + ] interaction = data["interaction"] pred = None if data.get("Ilam_spec") is not None: @@ -508,7 +478,14 @@ def _plot_marginal(self, ax, lam, lam_nm, trace, in_band, data) -> None: x_sp, y_sp = wlfreq(lam_spec) / _TWO_PI_PHZ, Ilam_spec * lam_spec**2 else: x_sp, y_sp = lam_spec / 1e-9, Ilam_spec - ax.plot(x_sp, _peak_norm(y_sp, sl_band), label="spectrum", c="C1", lw=1) + curves.append( + CurveView( + np.asarray(x_sp, float), + _peak_norm(y_sp, sl_band), + "measured", + "spectrum", + ) + ) pred = self._predict(lam_spec, Ilam_spec, interaction) if pred is not None: if freq: @@ -517,53 +494,38 @@ def _plot_marginal(self, ax, lam, lam_nm, trace, in_band, data) -> None: else: x_pr, y_pr = pred["lam_nm"], pred["marg_lam"] pb = (x_pr >= lo) & (x_pr <= hi) - ax.plot(x_pr, _peak_norm(y_pr, pb), label="predicted", c="C3", ls="--") + predicted = _peak_norm(y_pr, pb) + curves.append(CurveView(x_pr, predicted, "predicted", "predicted")) if correcting: - # the correction makes the trace marginal equal the predicted - # SHG autoconvolution; show that snap in the trace colour. - ax.plot( - x_pr, - _peak_norm(y_pr, pb), - c="C0", - ls="--", - lw=1.5, - label="trace marginal (corrected)", + curves.append( + CurveView( + x_pr, predicted, "corrected", "trace marginal (corrected)" + ) ) - # Centroid check: vertical lines. The spectrum centroid (C1) sits on the - # predicted-marginal centroid (C3) for PG/TG/SD, but at half it for SHG - # (whose marginal is centred at 2ω̄_S). + # Centroid check: vertical rules. The spectrum centroid sits on the + # predicted-marginal centroid for PG/TG/SD, but at half it for SHG (whose + # marginal is centred at 2ω̄_S). c_meas_nm, sigma_meas = self._measured_moments(lam, marg, in_band) to_x = (lambda nm: float(wlfreq(nm * 1e-9) / _TWO_PI_PHZ)) if freq else float + marks = [] if c_meas_nm is not None: - ax.axvline(to_x(c_meas_nm), c="C0", ls=":", lw=1, label="meas centroid") + marks.append((to_x(c_meas_nm), "retrieved")) if pred is not None: - ax.axvline( - to_x(pred["spectrum_centroid_nm"]), - c="C1", - ls="--", - lw=1, - label="spec centroid", - ) - ax.axvline( - to_x(pred["centroid_nm"]), c="C3", ls=":", lw=1, label="pred centroid" - ) - - self._annotate_checks(ax, c_meas_nm, sigma_meas, pred, interaction) - - if log: - ax.set_yscale("log") - ax.set_ylim(1e-3, 1.5) - else: - ax.set_ylim(0.0, 1.1) - if freq: - ax.set_xlim(self._f_lo(), self._f_hi()) - ax.set_xlabel("Frequency (PHz)") - else: - ax.set_xlim(lo, hi) - ax.set_xlabel("Wavelength (nm)") - ax.set_ylabel("Intensity (a.u.)") - ax.legend(fontsize="small") + marks.append((to_x(pred["spectrum_centroid_nm"]), "measured")) + marks.append((to_x(pred["centroid_nm"]), "predicted")) + + self._annotate_checks(c_meas_nm, sigma_meas, pred, interaction) + return LineView( + curves=tuple(curves), + title="Marginal against the spectrum", + xlabel="Frequency (PHz)" if freq else "Wavelength (nm)", + ylabel="Intensity (a.u.)", + xrange=(self._f_lo(), self._f_hi()) if freq else (lo, hi), + yrange=(1e-3, 1.5) if log else (0.0, 1.1), + log_y=log, + marks=tuple(marks), + ) def _f_lo(self) -> float: """Low frequency edge (PHz) of the trust region (from its long-λ bound).""" @@ -652,8 +614,8 @@ def _measured_moments(self, lam, marg, in_band): c = centroid(omega, weights) return float(wlfreq(c) / 1e-9), rms_width(omega, weights) - def _annotate_checks(self, ax, c_meas_nm, sigma_meas, pred, interaction) -> None: - """Write the numeric centroid/width QC summary to the status line + plot.""" + def _annotate_checks(self, c_meas_nm, sigma_meas, pred, interaction) -> None: + """Write the numeric centroid/width QC summary to the ready line.""" if pred is None or c_meas_nm is None: self.set_status("No independent spectrum: showing the trace marginal only.") return diff --git a/croak/plotting/screen.py b/croak/plotting/screen.py index 90563a2..346e765 100644 --- a/croak/plotting/screen.py +++ b/croak/plotting/screen.py @@ -45,6 +45,7 @@ "delay_marginal_view", "filter_view", "frequency_marginal_view", + "raw_trace_map", "residual_map", "spectral_view", "spectrogram_map", @@ -63,6 +64,10 @@ "fit", "truth-phase", "error", + "predicted", + "corrected", + "faint", + "boundary", ) @@ -131,8 +136,8 @@ class LineView: A logarithmic left axis. notes : tuple of (str, bool) Short annotations, each with whether it is a warning. - marks : tuple of float - x positions of dashed vertical rules. + marks : tuple of (float, str) + Vertical rules: x position and role (the role sets colour and style). """ curves: tuple[CurveView, ...] @@ -145,7 +150,7 @@ class LineView: right_range: tuple[float, float] | None = None log_y: bool = False notes: tuple[tuple[str, bool], ...] = field(default_factory=tuple) - marks: tuple[float, ...] = field(default_factory=tuple) + marks: tuple[tuple[float, str], ...] = field(default_factory=tuple) def _evenly(axis: NDArray[np.float64]) -> NDArray[np.float64]: @@ -417,7 +422,11 @@ def convergence_view(errors: ArrayLike, boundaries: Sequence[int] = ()) -> LineV xlabel="Iteration", ylabel="Error", log_y=True, - marks=tuple(float(edge) + 0.5 for edge in boundaries if 0 < edge < errors.size), + marks=tuple( + (float(edge) + 0.5, "boundary") + for edge in boundaries + if 0 < edge < errors.size + ), ) @@ -489,3 +498,54 @@ def filter_view(data: RetrievalPlotData) -> LineView: ylabel="Filter factor", xrange=(data.lam_min / 1e-9, data.lam_max / 1e-9), ) + + +SIGNAL_FRACTION = 1e-2 +"""A wavelength row holds signal if its peak exceeds this fraction of the scale.""" + + +def raw_trace_map( + delay_fs: ArrayLike, + wavelength_nm: ArrayLike, + trace: ArrayLike, + trust_nm: tuple[float, float], + *, + title: str = "Measured trace", +) -> MapView: + """Return a loaded trace (wavelength × delay) as a signed linear map. + + The colour scale is set by the signal inside ``trust_nm`` alone, so noise far + outside it (strongly amplified by a ``(λ/µm)^exp`` correction) cannot wash + the image out, and the view is cropped to the trusted rows that hold signal. + + Parameters + ---------- + delay_fs, wavelength_nm : array_like + The trace's axes (any order, any spacing). + trace : array_like, shape (len(wavelength_nm), len(delay_fs)) + The trace; negative after background subtraction is allowed. + trust_nm : tuple of float + The band whose signal sets the scale. + title : str + The view's title. + """ + lam = np.asarray(wavelength_nm, dtype=float) + trace = np.asarray(trace, dtype=float) + in_band = (lam >= trust_nm[0]) & (lam <= trust_nm[1]) + band = trace[in_band] if np.any(in_band) else trace + vmax = float(np.nanmax(np.abs(band))) or 1.0 + rows = lam[in_band & (trace.max(axis=1) > SIGNAL_FRACTION * vmax)] + x, y, values = uniform_image(delay_fs, lam, trace) + low = min(float(np.nanmin(values)), 0.0) + return MapView( + values=values, + x=x, + y=y, + levels=(low, vmax), + title=title, + xlabel="Delay (fs)", + ylabel="Wavelength (nm)", + unit="a.u.", + signed=True, + yrange=(float(rows.min()), float(rows.max())) if rows.size else None, + ) diff --git a/tests/gui/test_plots.py b/tests/gui/test_plots.py index 22808cc..d36f913 100644 --- a/tests/gui/test_plots.py +++ b/tests/gui/test_plots.py @@ -280,8 +280,8 @@ def test_plots_curves_can_be_thin_references_drawn_beneath(qtbot): def test_plots_marks_replace_the_previous_set(qtbot): plot = _shown(qtbot, LinePlot()) - plot.set_marks([1.0, 2.0]) - plot.set_marks([3.0], colour="#ff0000") + plot.set_marks([(1.0, ""), (2.0, "")]) + plot.set_marks([(3.0, "#ff0000")], dashed=False) assert [m.value() for m in plot.marks] == [3.0] diff --git a/tests/gui/test_stage_load.py b/tests/gui/test_stage_load.py index cc7c426..1514ab7 100644 --- a/tests/gui/test_stage_load.py +++ b/tests/gui/test_stage_load.py @@ -386,11 +386,11 @@ def test_stage_load_draw_handles_position_scans_and_out_of_band_data(stage): "trace": np.zeros((6, 5)), } stage._draw(data) - ax = stage.trace_canvas.figure.axes[0] - assert ax.get_title() == "Measured trace" - # τ = 2z/c: ±3 µm of stage travel is ±20 fs of delay (the mesh adds half a - # 10 fs cell either side), not the ±3e-6 a raw position axis would give - lo, hi = ax.get_xlim() + view = stage.trace_view + assert view.title.text() == "Measured trace" + # τ = 2z/c: ±3 µm of stage travel is ±20 fs of delay (each sample is a + # 10 fs pixel centred on its delay), not the ±3e-6 a raw position axis gives + lo, hi = view.plot.box.viewRange()[0] assert lo == pytest.approx(-25.0, abs=0.1) and hi == pytest.approx(25.0, abs=0.1) @@ -401,14 +401,9 @@ def test_stage_load_draw_handles_an_empty_trace(stage): "input_unit": "delay", "trace": np.zeros((0, 3)), } - called = [] - monkey = stage.trace_canvas.render - stage.trace_canvas.render = lambda fn: called.append(fn) - try: - stage._draw(data) - finally: - stage.trace_canvas.render = monkey - assert len(called) == 1 + stage._draw(data) + assert stage.trace_view.plot.data is None # said, not drawn + assert stage.trace_view.plot.message.isVisible() def test_stage_load_signature_ignores_reverse_trace(stage): diff --git a/tests/gui/test_stage_marginal_check.py b/tests/gui/test_stage_marginal_check.py index 79e0607..d4c9d78 100644 --- a/tests/gui/test_stage_marginal_check.py +++ b/tests/gui/test_stage_marginal_check.py @@ -52,9 +52,9 @@ def _enter(stage, data, entry="experimental"): stage.on_enter() -def _legend(canvas): - legend = canvas.figure.axes[0].get_legend() - return [t.get_text() for t in legend.get_texts()] +def _legend(view): + """The names of the curves the marginal view shows.""" + return [c.name for c in view.plot.curves] @pytest.fixture @@ -79,7 +79,7 @@ def test_stage_marginal_check_without_data_asks_for_a_trace(stage): stage.on_enter() assert stage.status_label.text() == "Load a trace first." stage._draw() # nothing to draw - assert stage.trace_canvas.figure.axes == [] + assert stage.trace_view.plot.data is None def test_stage_marginal_check_measured_trace_defaults_and_controls(stage): @@ -92,7 +92,7 @@ def test_stage_marginal_check_measured_trace_defaults_and_controls(stage): assert stage.exp_spin.value() == pytest.approx(3.5) assert stage.auto_btn.isEnabled() assert stage.status_label.text().startswith("PG marginal check") - assert "trace marginal" in _legend(stage.marg_canvas) + assert "trace marginal" in _legend(stage.marg_view) def test_stage_marginal_check_trust_is_kept_for_the_same_trace(stage): @@ -136,7 +136,7 @@ def test_stage_marginal_check_shg_correction_needs_a_spectrum(stage): assert stage.marginal_correct_check.isEnabled() stage.marginal_correct_check.setChecked(True) assert stage.state.preproc.marginal_correct is True - assert "trace marginal (corrected)" in _legend(stage.marg_canvas) + assert "trace marginal (corrected)" in _legend(stage.marg_view) assert "(exact)" in stage.status_label.text() # SHG predicts the width exactly @@ -145,18 +145,19 @@ def test_stage_marginal_check_wavelength_axis_and_log_scale(stage): _enter(stage, _data()) stage.axis_combo.setCurrentText("wavelength") stage.scale_combo.setCurrentText("log") - ax = stage.marg_canvas.figure.axes[0] - assert ax.get_xlabel() == "Wavelength (nm)" - assert ax.get_yscale() == "log" - assert ax.get_xlim() == pytest.approx(stage._trust_nm) + plot = stage.marg_view.plot + assert plot.item.getAxis("bottom").labelText == "Wavelength (nm)" + assert plot.log_y + assert plot.box.viewRange()[0] == pytest.approx(stage._trust_nm) + # the log window is 1e-3 … 1.5, in decades + assert plot.box.viewRange()[1] == pytest.approx([-3.0, np.log10(1.5)]) def test_stage_marginal_check_position_scan_and_blank_trace(stage): # A blank, position-scanned trace: the image has no in-band signal to # zoom to and the marginal has too few positive samples for its moments. _enter(stage, _data(trace=np.zeros((LAM.size, 21)), input_unit="position")) - ax = stage.trace_canvas.figure.axes[0] - assert ax.get_title() == "Measured trace" + assert stage.trace_view.title.text() == "Measured trace" assert stage.status_label.text() == ( "No independent spectrum: showing the trace marginal only." ) @@ -165,8 +166,8 @@ def test_stage_marginal_check_position_scan_and_blank_trace(stage): def test_stage_marginal_check_trace_outside_trust_scales_to_all_of_it(stage): _enter(stage, _data(), entry="synthetic") stage._on_trust(100.0, 200.0) # no sample in band - ax = stage.trace_canvas.figure.axes[0] - assert ax.get_title() == "Measured trace" + assert stage.trace_view.title.text() == "Measured trace" + assert stage.trace_view.plot.data is not None # -- prediction edge cases ------------------------------------------------------ @@ -206,7 +207,7 @@ def test_stage_marginal_check_width_bracket_flags(stage, sigma_meas, flag): "width_is_hard": False, "rms_width": 2.0, } - stage._annotate_checks(None, 801.0, sigma_meas, pred, "pg") + stage._annotate_checks(801.0, sigma_meas, pred, "pg") text = stage.status_label.text() assert text.startswith("PG marginal check") assert "(Δ +1.0 nm, hard)" in text @@ -224,7 +225,7 @@ def test_stage_marginal_check_sd_reports_only_the_lower_bound(stage, sigma_meas, "width_is_hard": False, "rms_width": 2.0, } - stage._annotate_checks(None, 800.0, sigma_meas, pred, "sd") + stage._annotate_checks(800.0, sigma_meas, pred, "sd") text = stage.status_label.text() assert "TL/soft" in text assert text.endswith(f"(σ_S) {mark}") diff --git a/tests/gui/test_wizard_flow.py b/tests/gui/test_wizard_flow.py index c7ce078..60c1fd5 100644 --- a/tests/gui/test_wizard_flow.py +++ b/tests/gui/test_wizard_flow.py @@ -2273,32 +2273,6 @@ def test_solver_switch_rescales_regularisation(qtbot): # -- responsive canvas --------------------------------------------------------- -@pytest.mark.parametrize( - ("size", "small"), [((1366, 768), True), ((1920, 1080), False)] -) -def test_canvas_density_follows_the_window_size(qtbot, size, small): - """Plot text shrinks in a small window and stays at full size in a large one.""" - w = Driver(qtbot) - w.start("synthetic") - w.synthetic.advance() # marginal check: two stacked (5, 4)-inch canvases - canvas = w.stages[1].marg_canvas - w.resize(*size) - w.show() - qtbot.waitExposed(w) - if small: - qtbot.waitUntil(lambda: canvas.scale.font < 0.8) - else: - qtbot.waitUntil(lambda: canvas.scale.font == 1.0) - # once the debounced re-plot has run, the title carries the scaled size - qtbot.waitUntil( - lambda: ( - bool(canvas.figure.axes) - and canvas.figure.axes[0].title.get_fontsize() - == pytest.approx(12.0 * canvas.scale.font) - ) - ) - - def test_canvas_replots_when_a_resize_changes_the_density(qtbot): canvas = MplCanvas(figsize=(12, 7)) qtbot.addWidget(canvas) @@ -2438,7 +2412,7 @@ def test_stage_plots_move_below_the_form_on_a_narrow_window(qtbot): assert not stage.sheet.narrow assert ( stage.form.geometry().right() - < stage.marg_canvas.mapTo(stage, stage.marg_canvas.rect().topLeft()).x() + < stage.marg_view.mapTo(stage, stage.marg_view.rect().topLeft()).x() ) w.resize(900, 768) qtbot.waitUntil(lambda: stage.sheet.narrow) diff --git a/tests/test_plotting_screen.py b/tests/test_plotting_screen.py index 01537cb..9ed3703 100644 --- a/tests/test_plotting_screen.py +++ b/tests/test_plotting_screen.py @@ -145,7 +145,7 @@ def test_screen_spectral_view_without_measurement_or_truth(data): def test_screen_convergence_view_marks_inner_handovers_only(): view = screen.convergence_view([1.0, 0.5, 0.2, 0.1], boundaries=[0, 2, 4]) - assert view.log_y and view.marks == (2.5,) + assert view.log_y and view.marks == ((2.5, "boundary"),) assert_allclose(view.curves[0].x, [1, 2, 3, 4]) @@ -160,3 +160,18 @@ def test_screen_marginals_and_filter(data): x = filt.curves[0].x assert np.all(np.diff(x) > 0) # sorted by wavelength assert filt.xrange == pytest.approx((700.0, 950.0)) + + +def test_screen_raw_trace_scales_by_the_trusted_band_and_crops_to_signal(): + lam = np.linspace(1100.0, 100.0, 11) # descending, as some files store it + delay = np.linspace(-20.0, 20.0, 5) + trace = np.zeros((11, 5)) + trace[lam == 500.0] = 2.0 # the signal, in the trusted band + trace[lam == 100.0] = 50.0 # amplified noise outside it + trace[lam == 300.0, 0] = -0.5 # a negative left by background subtraction + view = screen.raw_trace_map(delay, lam, trace, (190.0, 1000.0)) + assert view.levels == (-0.5, 2.0) and view.signed + assert view.yrange == (500.0, 500.0) + assert view.y[0] < view.y[-1] + quiet = screen.raw_trace_map(delay, lam, np.zeros((11, 5)), (190.0, 1000.0)) + assert quiet.levels == (0.0, 1.0) and quiet.yrange is None From 93614371ab5e3698886313968e7d4e03e904ed2a Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 22:45:10 +0000 Subject: [PATCH 10/41] Draw the synthetic and simulated previews on pyqtgraph The generator's pulse, spectrum and spectrogram (screen.pulse_field_views) and the simulated scan's linear and dB maps with its marginals (screen.simulated_trace_views) move out of the stages into Qt-free view data. An uneven axis is now resampled at its finest spacing, so a wavelength axis from an even frequency grid keeps its peak; the generator's spectrogram is cropped to the band holding the pulse. Prompts name Continue instead of the old footer buttons. Co-Authored-By: Claude Opus 5.5 --- croak/gui/stage_simulated.py | 116 ++++----------- croak/gui/stage_synthetic.py | 135 +++++------------ croak/plotting/screen.py | 235 +++++++++++++++++++++++++++++- tests/gui/test_stage_simulated.py | 13 +- tests/gui/test_stage_synthetic.py | 31 +--- tests/test_plotting_screen.py | 38 ++++- 6 files changed, 345 insertions(+), 223 deletions(-) diff --git a/croak/gui/stage_simulated.py b/croak/gui/stage_simulated.py index 96ca7f5..549ecb1 100644 --- a/croak/gui/stage_simulated.py +++ b/croak/gui/stage_simulated.py @@ -21,6 +21,7 @@ from PySide6.QtWidgets import ( QComboBox, QFileDialog, + QGridLayout, QHBoxLayout, QLineEdit, QPushButton, @@ -35,22 +36,18 @@ read_simulated_window_keys, read_simulated_z_positions, ) -from ..maths import wlfreq -from ..plotting import cmap_white, signed_pcolormesh +from ..plotting.screen import simulated_trace_views from ..session.params import SimulatedLoadParams from ..session.pipeline import ( assemble_simulated_load_data, assemble_simulated_tracedata, ) from .base import Stage -from .canvas import MplCanvas, add_wide_row, check, combo, group, with_toolbar +from .canvas import add_wide_row, check, combo, group +from .retrieval_views import ViewPanel from .stage_load import seed_preproc_defaults from .widgets import RangeControl -# Angular frequency (rad/s) -> ordinary frequency (PHz), matching the -# marginal-check stage's frequency axis so the two previews read identically. -_TWO_PI_PHZ = 2.0 * np.pi * 1e15 - # TG-FROG keeps the carrier (no doubling), so only the χ³ interactions apply. _INTERACTIONS = ["pg", "sd"] # Reference spectrum: the post-mask "beamlet" (the beam that gates) or the @@ -175,8 +172,8 @@ class StageSimulated(Stage): "preprocess (filtering + regrid) stages. Fully raw: no " "third-order correction; the window/thickness, delay-sign and " "known-spectrum choices still apply. Use it to sanity-check the " - "retrieval free of any preprocessing doubt — Back from Retrieve " - "reaches Preprocess for a regridded comparison.", + "retrieval free of any preprocessing doubt. For a regridded " + "comparison, untick it here and Continue again.", ), ], ), @@ -292,12 +289,24 @@ def __init__(self, state, adopt_geometry): form.addRow(self.raw_direct) self.controls.addWidget(box) - # Navigation (Back / "Load & continue →") and Help live in the shared - # wizard chrome, via the entry-page hooks below. + # Continue (which calls advance) and Help live in the window's top bar. self.controls.addStretch(1) - self.canvas = MplCanvas(figsize=(9, 7)) - self.set_plot_area(with_toolbar(self.canvas)) + self.views = { + "lin": ViewPanel("map", signed=True), + "db": ViewPanel("map"), + "delay": ViewPanel("line"), + "spectral": ViewPanel("line"), + } + grid = QGridLayout() + grid.setContentsMargins(0, 0, 0, 0) + grid.setHorizontalSpacing(14) + grid.setVerticalSpacing(10) + for index, view in enumerate(self.views.values()): + grid.addWidget(view, index // 2, index % 2) + plots = QWidget() + plots.setLayout(grid) + self.set_plot_area(plots) self.apply_help() @@ -527,80 +536,17 @@ def _preview(self): if self.thickness.isEnabled(): self.set_status( f"Loaded the {self.thickness.currentText()} slice — review the " - "trace, then Load & continue to filter it." + "trace, then Continue to check and filter it." ) else: - self.set_status("Review the trace, then Load & continue to filter it.") + self.set_status("Review the trace, then Continue to check and filter it.") def _draw(self, data): - # The simulation file stores an ω-density; the loader divides it by λ² so - # it can ride the (spectrometer) wavelength pipeline. Here we display the - # file-native ω-density on a frequency axis (×λ², ν = ω/2π) so the preview - # matches the file, the marginal-check screen and the retrieval — see - # stage_marginal_check.py. λ ascends as ν descends, so reorder to ascending ν. - lam = data["lam"] # m, ascending - delays = data["scanaxis"] / 1e-15 - freq = wlfreq(lam) / _TWO_PI_PHZ # PHz, descending in lam order - order = np.argsort(freq) # -> ascending frequency - freq = freq[order] - trace = (data["trace"] * lam[:, None] ** 2)[order] # ω-density - # Re-normalise to unit peak: ×λ² rescales the (unit-peak λ-density) trace - # to a tiny absolute level, which the dB view (log10 ∈ [−tracedb, 0]) - # would otherwise clip entirely. - peak = float(np.nanmax(np.abs(trace))) - if peak > 0: - trace = trace / peak - # rows with signal set the frequency limits; the peak row always - # qualifies, so this is never empty (a blank trace shows its full range) - sig = freq[trace.max(axis=1) > 1e-2] if peak > 0 else freq - - def plot(fig): - axd = fig.subplot_mosaic("ab\ncd") - # linear and dB views with the filtering-stage colour scheme (white→viridis - # positive, white→red negative). Simulated traces carry no detector noise, - # so the linear vmax is the full-range peak (no trust window needed). - cmap_pos = cmap_white("viridis") - signed_pcolormesh( - axd["a"], - delays, - freq, - trace, - db=False, - cmap_pos=cmap_pos, - vmax=1.0 if peak > 0 else None, - ) - axd["a"].set_title("Simulated FROG trace (lin)") - signed_pcolormesh( - axd["b"], delays, freq, trace, db=True, tracedb=40.0, cmap_pos=cmap_pos - ) - axd["b"].set_title("Simulated FROG trace (dB)") - for key in ("a", "b"): - axd[key].set_xlabel("Delay (fs)") - axd[key].set_ylabel("Frequency (PHz)") - axd[key].set_ylim(sig.min(), sig.max()) - # delay marginal - tmarg = trace.sum(axis=0) - tmarg = tmarg / tmarg.max() if tmarg.max() > 0 else tmarg - axd["c"].plot(delays, tmarg, c="C0") - axd["c"].set_xlabel("Delay (fs)") - axd["c"].set_ylabel("Delay marginal") - # spectral marginal (ω-density) + the known input spectrum, if provided - wmarg = trace.sum(axis=1) - wmarg = wmarg / wmarg.max() if wmarg.max() > 0 else wmarg - axd["d"].plot(freq, wmarg, c="C0", label="trace marginal") - if data["lam_spec"] is not None and data["Ilam_spec"] is not None: - lam_s = data["lam_spec"] - f_s = wlfreq(lam_s) / _TWO_PI_PHZ - os_ = np.argsort(f_s) - spec = (data["Ilam_spec"] * lam_s**2)[os_] # ω-density - spec = spec / spec.max() if spec.max() > 0 else spec - axd["d"].plot(f_s[os_], spec, c="C1", lw=1, label="input") - axd["d"].legend(fontsize="small") - axd["d"].set_xlabel("Frequency (PHz)") - axd["d"].set_ylabel("Spectral marginal (ω-density)") - axd["d"].set_xlim(sig.min(), sig.max()) - - self.canvas.render(plot) + """Preview the scan: linear and dB maps, delay and spectral marginals.""" + for view, shown in zip( + self.views.values(), simulated_trace_views(data), strict=True + ): + view.show_view(shown) # -- load (the entry-page forward action) ------------------------------- @@ -643,8 +589,8 @@ def advance(self): if params.raw_direct: # Build the as-simulated trace on its native grid and go straight to # retrieval, skipping the marginal-check + preprocess stages. The - # normal load_data above stays set (it carries the truth overlay and - # lets Back re-run the regrid path for a comparison). + # normal load_data above stays set (it carries the truth overlay, and + # unticking the option re-runs the regridded path for a comparison). try: td = assemble_simulated_tracedata(params) except Exception as exc: diff --git a/croak/gui/stage_synthetic.py b/croak/gui/stage_synthetic.py index 3084a38..dceba70 100644 --- a/croak/gui/stage_synthetic.py +++ b/croak/gui/stage_synthetic.py @@ -18,6 +18,7 @@ import numpy as np from PySide6.QtCore import QTimer +from PySide6.QtWidgets import QGridLayout, QWidget from .. import dispersion from ..constants import C @@ -25,19 +26,19 @@ from ..grid import Grid from ..interactions import INTERACTIONS, get_interaction from ..materials import MATERIALS +from ..plotting.screen import pulse_field_views, raw_trace_map from ..processing import TruthPulse from ..pulses import gaussian_pulse, sech_pulse from .base import Stage from .canvas import ( - MplCanvas, add_wide_row, check, combo, dspin, group, spin, - with_toolbar, ) +from .retrieval_views import ViewPanel from .stage_load import seed_preproc_defaults from .widgets import RangeControl, ValueControl @@ -176,8 +177,21 @@ def __init__(self, state): self._seed_sampling() - self.canvas = MplCanvas(figsize=(9, 7)) - self.set_plot_area(with_toolbar(self.canvas)) + self.views = { + "temporal": ViewPanel("line", right=True), + "spectral": ViewPanel("line", right=True), + "spectrogram": ViewPanel("map"), + "trace": ViewPanel("map", signed=True), + } + grid = QGridLayout() + grid.setContentsMargins(0, 0, 0, 0) + grid.setHorizontalSpacing(14) + grid.setVerticalSpacing(10) + for index, view in enumerate(self.views.values()): + grid.addWidget(view, index // 2, index % 2) + plots = QWidget() + plots.setLayout(grid) + self.set_plot_area(plots) # -- lifecycle ---------------------------------------------------------- def on_enter(self): @@ -432,105 +446,24 @@ def _recompute(self): self._last = (grid, ew, omega0) # show the trace as it will be handed on (with any noise applied) self._draw(grid, ew, omega0, lam_sig, delays, self._add_noise(trace)) - self.set_status("Adjust parameters, then Generate to run retrieval.") + self.set_status("Adjust the pulse and the trace, then Continue to check it.") def _draw(self, grid, ew, omega0, lam_sig, delays, trace): - def plot(fig): - axd = fig.subplot_mosaic("ab\ncd") - self._plot_temporal(axd["a"], grid, ew) - self._plot_spectral(axd["b"], grid, ew, omega0) - self._plot_spectrogram(axd["c"], grid, ew, omega0) - self._plot_frog(axd["d"], lam_sig, delays, trace) - - self.canvas.render(plot) - - @staticmethod - def _masked_phase(intensity, field, frac=1e-3): - phase = np.unwrap(np.angle(field)) - mask = intensity < intensity.max() * frac - phase = phase - phase[int(np.argmax(intensity))] - phase[mask] = np.nan - return phase - - def _plot_temporal(self, ax, grid, ew): - Et = grid.ifft(ew) - inten = np.abs(Et) ** 2 - inten = inten / inten.max() if inten.max() > 0 else inten - t = grid.t / 1e-15 - ax.plot(t, inten, c="C0") - axp = ax.twinx() - axp.plot(t, self._masked_phase(inten, Et), c="C1", lw=1) - axp.set_ylabel("Phase (rad)", color="C1") - # zoom to the pulse - sig = t[inten > 1e-3] - if sig.size: - pad = 0.2 * (sig.max() - sig.min() + 1e-9) - ax.set_xlim(sig.min() - pad, sig.max() + pad) - ax.set_xlabel("Time (fs)") - ax.set_ylabel("Intensity") - ax.set_title("Temporal") - - def _plot_spectral(self, ax, grid, ew, omega0): - inten = np.abs(ew) ** 2 - inten = inten / inten.max() if inten.max() > 0 else inten - omega_abs = grid.omega + omega0 - pos = omega_abs > 0 - lam = 2 * math.pi * C / omega_abs[pos] / 1e-9 - order = np.argsort(lam) - lam = lam[order] - Ip = inten[pos][order] - phase = self._masked_phase(inten, ew)[pos][order] - ax.plot(lam, Ip, c="C0") - axp = ax.twinx() - axp.plot(lam, phase, c="C1", lw=1) - axp.set_ylabel("Phase (rad)", color="C1") - sig = lam[Ip > 1e-3] - if sig.size: - pad = 0.2 * (sig.max() - sig.min() + 1e-9) - ax.set_xlim(sig.min() - pad, sig.max() + pad) - ax.set_xlabel("Wavelength (nm)") - ax.set_ylabel("Intensity") - ax.set_title("Spectral") - - def _plot_spectrogram(self, ax, grid, ew, omega0, n_t=160): - """A Gabor spectrogram of the field (time × wavelength).""" - Et = grid.ifft(ew) - t = grid.t - inten = np.abs(Et) ** 2 - sig = t[inten > inten.max() * 1e-3] if inten.max() > 0 else t - tmin = sig.min() - 0.3 * (sig.max() - sig.min() + 1e-15) - tmax = sig.max() + 0.3 * (sig.max() - sig.min() + 1e-15) - tcs = np.linspace(tmin, tmax, n_t) - fw = max((tmax - tmin) / 30.0, grid.dt * 4) - S = np.empty((grid.n, n_t)) - for i, tc in enumerate(tcs): - gate = np.exp(-0.5 * ((t - tc) / fw) ** 2) - sp = np.fft.fftshift(np.fft.fft(np.fft.ifftshift(Et * gate))) - S[:, i] = np.abs(sp) ** 2 - omega_abs = grid.omega + omega0 - pos = omega_abs > 0 - S = S[pos] * (omega_abs[pos] ** 2)[:, None] - lam = 2 * math.pi * C / omega_abs[pos] / 1e-9 - order = np.argsort(lam) - S = S[order] - lam = lam[order] - S = S / S.max() if S.max() > 0 else S - ax.pcolormesh(tcs / 1e-15, lam, S, shading="auto") - lam_sig = lam[(S.max(axis=1) > 1e-2)] - if lam_sig.size: - ax.set_ylim(lam_sig.min(), lam_sig.max()) - ax.set_xlabel("Time (fs)") - ax.set_ylabel("Wavelength (nm)") - ax.set_title("Spectrogram") - - def _plot_frog(self, ax, lam_sig, delays, trace): - ax.pcolormesh(delays / 1e-15, lam_sig / 1e-9, trace, shading="auto") - sig = lam_sig[(trace.max(axis=1) > 1e-2)] / 1e-9 - if sig.size: - ax.set_ylim(sig.min(), sig.max()) - ax.set_xlabel("Delay (fs)") - ax.set_ylabel("Wavelength (nm)") - ax.set_title("Synthetic FROG trace") + """Show the pulse, its spectrum and spectrogram, and its trace.""" + temporal, spectral, gram = pulse_field_views(grid, ew, omega0) + self.views["temporal"].show_view(temporal) + self.views["spectral"].show_view(spectral) + self.views["spectrogram"].show_view(gram) + self.views["trace"].show_view( + raw_trace_map( + delays / 1e-15, + lam_sig / 1e-9, + trace, + # a synthetic trace has no out-of-band noise: trust all of it + (0.0, float("inf")), + title="Synthetic FROG trace", + ) + ) # -- generate (the entry-page forward action) --------------------------- diff --git a/croak/plotting/screen.py b/croak/plotting/screen.py index 346e765..b5b9b87 100644 --- a/croak/plotting/screen.py +++ b/croak/plotting/screen.py @@ -45,8 +45,10 @@ "delay_marginal_view", "filter_view", "frequency_marginal_view", + "pulse_field_views", "raw_trace_map", "residual_map", + "simulated_trace_views", "spectral_view", "spectrogram_map", "temporal_view", @@ -153,12 +155,23 @@ class LineView: marks: tuple[tuple[float, str], ...] = field(default_factory=tuple) +MAX_SAMPLES = 2048 +"""The most samples an evenly resampled image axis gets.""" + + def _evenly(axis: NDArray[np.float64]) -> NDArray[np.float64]: - """``axis`` if its steps are equal (to 0.1 %), else an evenly spaced copy.""" + """``axis`` if its steps are equal (to 0.1 %), else an evenly spaced copy. + + The copy keeps the finest spacing of ``axis`` (up to :data:`MAX_SAMPLES` + points), so resampling loses no detail where the samples are densest — a + wavelength axis from an even frequency grid is far denser at short + wavelengths than at long ones. + """ steps = np.diff(axis) if axis.size < 3 or np.allclose(steps, steps[0], rtol=1e-3, atol=0.0): return axis - return np.linspace(axis[0], axis[-1], axis.size) + count = int(np.ceil((axis[-1] - axis[0]) / steps.min())) + 1 + return np.linspace(axis[0], axis[-1], int(np.clip(count, axis.size, MAX_SAMPLES))) def uniform_image( @@ -549,3 +562,221 @@ def raw_trace_map( signed=True, yrange=(float(rows.min()), float(rows.max())) if rows.size else None, ) + + +# A field sample is part of the pulse above this fraction of the peak intensity; +# the views zoom to those samples and blank the phase elsewhere, where it is noise. +PULSE_FRACTION = 1e-3 +# Each side of the pulse gets this fraction of its extent as margin. +PULSE_MARGIN = 0.2 +SPECTROGRAM_COLUMNS = 160 +"""Gate positions of the generator's spectrogram.""" + + +def _masked_phase(intensity: NDArray[np.float64], field: NDArray[np.complex128]): + """Unwrapped phase, zero at the peak and NaN where the pulse is absent.""" + phase = np.unwrap(np.angle(field)) + phase = phase - phase[int(np.argmax(intensity))] + phase[intensity < intensity.max() * PULSE_FRACTION] = np.nan + return phase + + +def _unit(values: NDArray[np.float64]) -> NDArray[np.float64]: + peak = float(values.max()) if values.size else 0.0 + return values / peak if peak > 0 else values + + +def _zoom(x: NDArray[np.float64], keep: NDArray[np.bool_]): + """``x``'s extent where ``keep``, widened by :data:`PULSE_MARGIN` (or None).""" + sig = x[keep] + if not sig.size: + return None + pad = PULSE_MARGIN * (sig.max() - sig.min() + 1e-9) + return float(sig.min() - pad), float(sig.max() + pad) + + +def pulse_field_views( + grid, ew: ArrayLike, omega0: float +) -> tuple[LineView, LineView, MapView]: + """Return a known pulse as the generator shows it: time, spectrum, spectrogram. + + The time and spectral views carry the phase (zero at the peak, blank where + the intensity is below :data:`PULSE_FRACTION` of it) on a right axis, and + zoom to the pulse. The spectrogram gates the field with a Gaussian a + thirtieth of the pulse's span wide (at least four samples), at + :data:`SPECTROGRAM_COLUMNS` positions over the pulse and a 30 % margin, and + is a wavelength density, peak-normalised. + + Parameters + ---------- + grid : Grid + The field's grid. + ew : array_like + The spectral field on ``grid``. + omega0 : float + The carrier angular frequency (rad/s). + """ + ew = np.asarray(ew, dtype=complex) + et = grid.ifft(ew) + it = _unit(np.abs(et) ** 2) + t = grid.t / 1e-15 + temporal = LineView( + curves=( + CurveView(t, it, "retrieved", "intensity"), + CurveView(t, _masked_phase(it, et), "phase", "phase", True), + ), + title="Temporal", + xlabel="Time (fs)", + ylabel="Intensity", + right_label="Phase (rad)", + xrange=_zoom(t, it > PULSE_FRACTION), + yrange=(0.0, 1.05), + ) + iw = _unit(np.abs(ew) ** 2) + omega_abs = grid.omega + omega0 + pos = omega_abs > 0 + lam = wlfreq(omega_abs[pos]) / 1e-9 + order = np.argsort(lam) + lam, ip = lam[order], iw[pos][order] + spectral = LineView( + curves=( + CurveView(lam, ip, "retrieved", "intensity"), + CurveView(lam, _masked_phase(iw, ew)[pos][order], "phase", "phase", True), + ), + title="Spectral", + xlabel="Wavelength (nm)", + ylabel="Intensity", + right_label="Phase (rad)", + xrange=_zoom(lam, ip > PULSE_FRACTION), + yrange=(0.0, 1.05), + ) + return temporal, spectral, _gabor(grid, et, omega0) + + +def _gabor(grid, et: NDArray[np.complex128], omega0: float) -> MapView: + """Return the generator's spectrogram of the temporal field ``et``.""" + t = grid.t + inten = np.abs(et) ** 2 + sig = t[inten > inten.max() * PULSE_FRACTION] if inten.max() > 0 else t + span = sig.max() - sig.min() + 1e-15 + centres = np.linspace( + sig.min() - 0.3 * span, sig.max() + 0.3 * span, SPECTROGRAM_COLUMNS + ) + width = max((centres[-1] - centres[0]) / 30.0, grid.dt * 4) + s = np.empty((grid.n, centres.size)) + for i, centre in enumerate(centres): + gate = np.exp(-0.5 * ((t - centre) / width) ** 2) + s[:, i] = np.abs(np.fft.fftshift(np.fft.fft(np.fft.ifftshift(et * gate)))) ** 2 + omega_abs = grid.omega + omega0 + pos = omega_abs > 0 + # |E(ω)|² dω = |E(λ)|² dλ: a wavelength density carries the ω² Jacobian. + s = s[pos] * (omega_abs[pos] ** 2)[:, None] + lam = wlfreq(omega_abs[pos]) / 1e-9 + s = _unit(s) + has_signal = s.max(axis=1) > SIGNAL_FRACTION + rows = lam[has_signal] + if rows.size: + # Draw only the band that holds the pulse (and a margin): positive + # frequencies reach far into the infrared, where nothing is. + pad = PULSE_MARGIN * (rows.max() - rows.min() + 1e-9) + keep = (lam >= rows.min() - pad) & (lam <= rows.max() + pad) + lam, s = lam[keep], s[keep] + x, y, values = uniform_image(centres / 1e-15, lam, s) + return MapView( + values=values, + x=x, + y=y, + levels=(0.0, 1.0), + title="Spectrogram", + xlabel="Time (fs)", + ylabel="Wavelength (nm)", + unit="a.u.", + yrange=(float(rows.min()), float(rows.max())) if rows.size else None, + ) + + +SIMULATED_DB = 40.0 +"""Dynamic range of a simulated trace's logarithmic view, dB (no detector noise).""" + + +def simulated_trace_views( + data: dict, +) -> tuple[MapView, MapView, LineView, LineView]: + """Return a loaded simulated scan as its loader previews it. + + The simulation stores an ω-density, which the loader divides by λ² to ride + the wavelength pipeline; the preview undoes that (×λ², ν = ω/2π) so it shows + the file's own density against frequency, as the marginal check and the + retrieval do. It is then put on a unit peak (×λ² leaves a tiny absolute + level, which the logarithmic view would clip), shown linear and over + :data:`SIMULATED_DB`, with its delay and spectral marginals (the latter with + the known input spectrum, when the file has one). The view is cropped to the + rows that hold signal. + + Parameters + ---------- + data : dict + The loader's ``load_data``: ``lam`` (m, ascending), ``scanaxis`` (s), + ``trace`` (λ-density) and optionally ``lam_spec``/``Ilam_spec``. + + Returns + ------- + tuple + The linear map, the logarithmic map, the delay marginal and the + spectral marginal. + """ + lam = np.asarray(data["lam"], dtype=float) + delays = np.asarray(data["scanaxis"], dtype=float) / 1e-15 + freq = wlfreq(lam) / _TWOPI_PHZ + order = np.argsort(freq) + freq = freq[order] + trace = (np.asarray(data["trace"], dtype=float) * lam[:, None] ** 2)[order] + peak = float(np.nanmax(np.abs(trace))) + if peak > 0: + trace = trace / peak + rows = freq[trace.max(axis=1) > SIGNAL_FRACTION] if peak > 0 else freq + window = (float(rows.min()), float(rows.max())) + x, y, lin = uniform_image(delays, freq, trace) + with np.errstate(divide="ignore"): + db = np.clip(10 * np.log10(np.maximum(lin, 0.0)), -SIMULATED_DB, 0.0) + common = {"x": x, "y": y, "xlabel": "Delay (fs)", "ylabel": "Frequency (PHz)"} + linear = MapView( + values=lin, + levels=(min(float(lin.min()), 0.0), 1.0), + title="Simulated FROG trace (lin)", + unit="a.u.", + signed=True, + yrange=window, + **common, + ) + logarithmic = MapView( + values=db, + levels=(-SIMULATED_DB, 0.0), + title="Simulated FROG trace (dB)", + unit="dB", + yrange=window, + **common, + ) + delay_marginal = LineView( + curves=(CurveView(delays, _unit(trace.sum(axis=0)), "retrieved", "delay"),), + title="Delay marginal", + xlabel="Delay (fs)", + ylabel="Marginal (a.u.)", + yrange=(0.0, 1.05), + ) + curves = [CurveView(freq, _unit(trace.sum(axis=1)), "retrieved", "trace marginal")] + if data.get("lam_spec") is not None and data.get("Ilam_spec") is not None: + lam_s = np.asarray(data["lam_spec"], dtype=float) + f_s = wlfreq(lam_s) / _TWOPI_PHZ + order_s = np.argsort(f_s) + spectrum = (np.asarray(data["Ilam_spec"], dtype=float) * lam_s**2)[order_s] + curves.append(CurveView(f_s[order_s], _unit(spectrum), "measured", "input")) + spectral_marginal = LineView( + curves=tuple(curves), + title="Spectral marginal (ω-density)", + xlabel="Frequency (PHz)", + ylabel="Marginal (a.u.)", + xrange=window, + yrange=(0.0, 1.05), + ) + return linear, logarithmic, delay_marginal, spectral_marginal diff --git a/tests/gui/test_stage_simulated.py b/tests/gui/test_stage_simulated.py index ba9161c..120c65e 100644 --- a/tests/gui/test_stage_simulated.py +++ b/tests/gui/test_stage_simulated.py @@ -50,9 +50,9 @@ def test_stage_simulated_on_enter_previews_the_chosen_file(wizard, simulated_sca assert sim.can_advance() # a legacy file has no thickness slices: the generic prompt assert sim.status_label.text() == ( - "Review the trace, then Load & continue to filter it." + "Review the trace, then Continue to check and filter it." ) - titles = [ax.get_title() for ax in sim.canvas.figure.axes] + titles = [view.title.text() for view in sim.views.values()] assert "Simulated FROG trace (lin)" in titles @@ -219,10 +219,9 @@ def test_stage_simulated_draw_handles_a_blank_trace_and_spectrum(wizard): lam = np.linspace(700e-9, 900e-9, 6) sim = wizard.simulated sim._draw(_sim_data(lam, np.zeros((6, 4)), lam, np.zeros(6))) - axd = {ax.get_title(): ax for ax in sim.canvas.figure.axes} - assert "Simulated FROG trace (lin)" in axd - legend = sim.canvas.figure.axes[3].get_legend() - assert [t.get_text() for t in legend.get_texts()] == ["trace marginal", "input"] + marginal = sim.views["spectral"].plot + assert sim.views["lin"].title.text() == "Simulated FROG trace (lin)" + assert [c.name for c in marginal.curves] == ["trace marginal", "input"] def test_stage_simulated_draw_skips_a_spectrum_without_intensity(wizard): @@ -230,4 +229,4 @@ def test_stage_simulated_draw_skips_a_spectrum_without_intensity(wizard): trace = np.outer(np.hanning(6), np.hanning(4)) sim = wizard.simulated sim._draw(_sim_data(lam, trace, lam_spec=lam, Ilam_spec=None)) - assert sim.canvas.figure.axes[3].get_legend() is None + assert [c.name for c in sim.views["spectral"].plot.curves] == ["trace marginal"] diff --git a/tests/gui/test_stage_synthetic.py b/tests/gui/test_stage_synthetic.py index 0d28ccd..6a0315e 100644 --- a/tests/gui/test_stage_synthetic.py +++ b/tests/gui/test_stage_synthetic.py @@ -7,9 +7,7 @@ pytest.importorskip("PySide6") -from matplotlib.figure import Figure -from croak.grid import Grid from croak.gui.stage_synthetic import StageSynthetic from croak.gui.state import WizardState @@ -125,7 +123,7 @@ def fail(): monkeypatch.setattr(stage, "_pulse", fail) stage.on_enter() assert stage.status_label.text() == "Generation failed: bad grid" - assert not stage.canvas.rendered + assert stage.views["trace"].plot.data is None # nothing drawn def test_generate_failure_stays_on_the_page(stage, monkeypatch): @@ -157,25 +155,6 @@ def test_generate_hands_the_trace_and_truth_on(stage): assert stage.state.load.interaction == stage.interaction.currentText() -def test_plot_helpers_tolerate_an_empty_field(stage): - grid = Grid(64, dt=1e-15) - zeros = np.zeros(grid.n, dtype=complex) - fig = Figure() - ax = fig.add_subplot() - stage._plot_temporal(ax, grid, zeros) - stage._plot_spectral(fig.add_subplot(), grid, zeros, 2.35e15) - # Nothing above threshold: no zoom, the axis spans the whole window. - lo, hi = ax.get_xlim() - assert lo <= grid.t.min() / 1e-15 and hi >= grid.t.max() / 1e-15 - spec_ax = fig.add_subplot() - stage._plot_spectrogram(spec_ax, grid, zeros, 2.35e15, n_t=8) - assert spec_ax.get_title() == "Spectrogram" - trace = np.zeros((10, 5)) - frog_ax = fig.add_subplot() - stage._plot_frog(frog_ax, np.linspace(7e-7, 9e-7, 10), np.arange(5.0), trace) - assert frog_ax.get_title() == "Synthetic FROG trace" - - def test_additive_noise_alone(stage): trace = np.linspace(0.0, 1.0, 50).reshape(5, 10) stage.noise_add.set_value(0.05) @@ -185,8 +164,8 @@ def test_additive_noise_alone(stage): def test_preview_draws_the_four_panels(stage): stage.on_enter() - assert stage.canvas.rendered - titles = {ax.get_title() for ax in stage.canvas.figure.axes} - assert {"Temporal", "Spectral", "Spectrogram", "Synthetic FROG trace"} <= titles - assert stage.status_label.text().startswith("Adjust parameters") + titles = {view.title.text() for view in stage.views.values()} + assert titles == {"Temporal", "Spectral", "Spectrogram", "Synthetic FROG trace"} + assert stage.views["trace"].plot.data is not None + assert stage.status_label.text().startswith("Adjust the pulse") assert stage._last is not None diff --git a/tests/test_plotting_screen.py b/tests/test_plotting_screen.py index 9ed3703..ac29094 100644 --- a/tests/test_plotting_screen.py +++ b/tests/test_plotting_screen.py @@ -52,10 +52,17 @@ def test_screen_uniform_image_reverses_and_resamples(): x, y, values = screen.uniform_image( [0.0, 1.0, 3.0], [1.0, 0.0], [[0.0, 1.0, 3.0], [0.0, 2.0, 6.0]] ) - assert_allclose(x, [0.0, 1.5, 3.0]) + # Resampled at the finest spacing (1), so no sample is lost. + assert_allclose(x, [0.0, 1.0, 2.0, 3.0]) assert_allclose(y, [0.0, 1.0]) # Linear in x along each row: resampling moves no feature. - assert_allclose(values, [[0.0, 3.0, 6.0], [0.0, 1.5, 3.0]]) + assert_allclose(values, [[0.0, 2.0, 4.0, 6.0], [0.0, 1.0, 2.0, 3.0]]) + + +def test_screen_uniform_image_caps_its_samples(): + x = np.concatenate([[0.0, 1e-6], np.linspace(1.0, 2.0, 5)]) + xe, _, values = screen.uniform_image(x, [0.0, 1.0], np.ones((2, x.size))) + assert xe.size == screen.MAX_SAMPLES and values.shape == (2, screen.MAX_SAMPLES) @pytest.mark.parametrize( @@ -175,3 +182,30 @@ def test_screen_raw_trace_scales_by_the_trusted_band_and_crops_to_signal(): assert view.y[0] < view.y[-1] quiet = screen.raw_trace_map(delay, lam, np.zeros((11, 5)), (190.0, 1000.0)) assert quiet.levels == (0.0, 1.0) and quiet.yrange is None + + +def test_screen_pulse_views_zoom_to_the_pulse_and_blank_its_wings(): + g = Grid(128, dt=0.5e-15) + ew = gaussian_pulse(g, 7e-15, phases=[20e-30]) + temporal, spectral, gram = screen.pulse_field_views(g, ew, float(wlfreq(800e-9))) + assert [c.role for c in temporal.curves] == ["retrieved", "phase"] + lo, hi = temporal.xrange + assert -40 < lo < 0 < hi < 40 # zoomed to a 7 fs pulse, not the whole window + phase = temporal.curves[1].y + assert np.isnan(phase[0]) and np.isfinite(phase[np.argmax(temporal.curves[0].y)]) + assert spectral.xlabel == "Wavelength (nm)" + full = spectral.curves[0].x + assert full.min() < spectral.xrange[0] < 800 < spectral.xrange[1] < full.max() + # The even resampling of the uneven wavelength axis keeps the peak to 1 %. + assert gram.levels == (0.0, 1.0) + assert gram.values.max() == pytest.approx(1.0, abs=1e-2) + assert gram.yrange is not None + + +def test_screen_pulse_views_tolerate_an_empty_field(): + g = Grid(64, dt=1e-15) + temporal, spectral, gram = screen.pulse_field_views( + g, np.zeros(g.n, dtype=complex), 2.35e15 + ) + assert temporal.xrange is None and spectral.xrange is None + assert gram.yrange is None and gram.values.max() == 0.0 From 3ca67617e7dfd90338f8117dfdea5cd71a36f576 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 22:48:04 +0000 Subject: [PATCH 11/41] Draw the preprocessing preview on pyqtgraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit screen.filter_views gives the six maps (measured, filtered and regridded, in dB and linear on the shared filter_view_vmax scale), thinned for display; Prepare draws them in a 2×3 grid whose wavelength and delay axes are linked. Co-Authored-By: Claude Opus 5.5 --- croak/gui/stage_preprocess.py | 55 ++++++++++---------- croak/plotting/screen.py | 80 ++++++++++++++++++++++++++++++ tests/gui/test_stage_preprocess.py | 16 +++--- tests/gui/test_wizard_flow.py | 2 +- tests/test_plotting_screen.py | 50 +++++++++++++++++++ 5 files changed, 167 insertions(+), 36 deletions(-) diff --git a/croak/gui/stage_preprocess.py b/croak/gui/stage_preprocess.py index 2349160..572a80c 100644 --- a/croak/gui/stage_preprocess.py +++ b/croak/gui/stage_preprocess.py @@ -26,22 +26,22 @@ import shiboken6 from PySide6.QtCore import QTimer -from PySide6.QtWidgets import QHBoxLayout, QPushButton, QWidget +from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QPushButton, QWidget -from .. import io, plotting +from .. import io +from ..plotting.screen import FILTER_PANELS, filter_views from ..preprocess import suggested_tau_exclude from ..session import pipeline from .base import Stage from .canvas import ( - MplCanvas, add_wide_row, check, combo, dspin, group, spin, - with_toolbar, ) +from .retrieval_views import ViewPanel from .widgets import RangeControl, SciSpinBox from .worker import PreprocessWorker @@ -199,7 +199,6 @@ def __init__(self, state): self._seeding = False # Off-thread preprocessing: the plotting redraw (not the math) is the # bottleneck, but both used to block the UI thread on every change. - self._view = plotting.FrogFilterView() self._req_id = 0 # bumped per recompute; stale worker results are ignored self._workers: list[PreprocessWorker] = [] self._pending: tuple | None = None # (params, data) for the exact refine @@ -409,8 +408,30 @@ def __init__(self, state): self.controls.addWidget(self.reset_btn) self.controls.addStretch(1) - self.canvas = MplCanvas(figsize=(9, 5)) - self.set_plot_area(with_toolbar(self.canvas)) + # The filter preview: measured, filtered and regridded traces in dB and + # linear. The wavelength-domain maps share their axes, as do all six + # delay axes, so a zoom on one follows on the others. + self.views = { + key: ViewPanel("map", signed=not db) + for key, _title, _which, _domain, db in FILTER_PANELS + } + grid = QGridLayout() + grid.setContentsMargins(0, 0, 0, 0) + grid.setHorizontalSpacing(14) + grid.setVerticalSpacing(10) + for index, view in enumerate(self.views.values()): + grid.addWidget(view, index // 3, index % 3) + first = self.views["measured_db"].plot + for key, view in self.views.items(): + if key == "measured_db": + continue + view.plot.setYLink(first) + if not key.startswith("regridded"): + view.plot.setXLink(first) + self.views["regridded_lin"].plot.setXLink(self.views["regridded_db"].plot) + plots = QWidget() + plots.setLayout(grid) + self.set_plot_area(plots) self.apply_help() @@ -651,28 +672,12 @@ def _discard_worker(self, worker): def _show_tracedata(self, td): """Store and draw a freshly computed trace (main thread).""" self.state.set_tracedata(td) - if self._view.attached: - # Fast path: the artists exist, so push the data and repaint. - self._view.update(td) - self.canvas.draw_idle() - else: - self.canvas.render(self._replot_view) + for key, view in filter_views(td).items(): + self.views[key].show_view(view) self.set_status( f"Regridded to {td.grid.n}×{td.delays.size}. Proceed to Retrieve." ) - def _replot_view(self, fig) -> None: - """(Re)build the filter view in ``fig`` and fill it from the current trace. - - Handed to :meth:`~croak.gui.canvas.MplCanvas.render`, which re-runs it when - a resize changes the text density; the view is rebuilt on the cleared - figure and refilled from ``state.tracedata`` (nothing to fill if none yet). - """ - self._view.attach(fig) - td = self.state.tracedata - if td is not None: - self._view.update(td) - def _on_preprocess_done(self, td, req_id, was_fast): if req_id != self._req_id: return # a newer request superseded this one diff --git a/croak/plotting/screen.py b/croak/plotting/screen.py index b5b9b87..138ef02 100644 --- a/croak/plotting/screen.py +++ b/croak/plotting/screen.py @@ -35,6 +35,7 @@ _truth_spectral_phase, _truth_spectrum, ) +from .traces import _decimate_for_display, filter_view_vmax from .units import _TWOPI_PHZ __all__ = [ @@ -44,6 +45,7 @@ "convergence_view", "delay_marginal_view", "filter_view", + "filter_views", "frequency_marginal_view", "pulse_field_views", "raw_trace_map", @@ -780,3 +782,81 @@ def simulated_trace_views( yrange=(0.0, 1.05), ) return linear, logarithmic, delay_marginal, spectral_marginal + + +FILTER_PANELS = ( + # key, title, which trace, domain, logarithmic + ("measured_db", "Measured (dB)", "measured", "wavelength", True), + ("filtered_db", "Filtered (dB)", "filtered", "wavelength", True), + ("regridded_db", "Regridded (dB)", "regridded", "frequency", True), + ("measured_lin", "Measured (lin)", "measured", "wavelength", False), + ("filtered_lin", "Filtered (lin)", "filtered", "wavelength", False), + ("regridded_lin", "Regridded (lin)", "regridded", "frequency", False), +) +"""The preprocessing view's six maps, in reading order (two rows of three).""" + +DISPLAY_CAP = 1000 +"""The most samples a preprocessing map keeps per side (screens are narrower).""" + + +def filter_views(td, *, tracedb: float = 30.0) -> dict[str, MapView]: + """Return the preprocessing view: measured, filtered and regridded traces. + + Each trace is shown in dB (``tracedb`` below its peak, positive part) and + linear (signed), against delay: the measured and filtered traces against + wavelength, the regridded one against signal frequency. The linear maps + share :func:`~croak.plotting.filter_view_vmax`, so their colours compare. + Large traces are thinned to :data:`DISPLAY_CAP` samples per side for + drawing only. + + Parameters + ---------- + td : TraceData + The preprocessed trace (:func:`croak.preprocess.load_and_clean`). + tracedb : float + The logarithmic maps' dynamic range, dB. + + Returns + ------- + dict of str to MapView + Keyed as :data:`FILTER_PANELS`. + """ + lam_nm = np.asarray(td.lam_frog, dtype=float) / 1e-9 + freq = (np.asarray(td.omega) + td.omega0_trace) / _TWOPI_PHZ + sources = { + "measured": (lam_nm, td.tau_meas / 1e-15, td.Ifrog_meas), + "filtered": (lam_nm, td.tau_filt / 1e-15, td.Ifrog_filt), + "regridded": (freq, td.delays / 1e-15, td.trace), + } + vmax = filter_view_vmax(td) + views = {} + for key, title, which, domain, db in FILTER_PANELS: + x, delays, trace = sources[which] + image, x, delays = _decimate_for_display( + np.asarray(trace, dtype=float).T, + np.asarray(x, dtype=float), + np.asarray(delays, dtype=float), + cap=DISPLAY_CAP, + ) + xs, ys, values = uniform_image(x, delays, image) + if db: + peak = float(np.nanmax(values)) or 1.0 + with np.errstate(divide="ignore", invalid="ignore"): + values = np.clip( + 10 * np.log10(np.maximum(values, 0.0) / peak), -tracedb, 0.0 + ) + levels, unit = (-tracedb, 0.0), "dB" + else: + levels, unit = (min(float(np.nanmin(values)), 0.0), vmax), "a.u." + views[key] = MapView( + values=values, + x=xs, + y=ys, + levels=levels, + title=title, + xlabel="Frequency (PHz)" if domain == "frequency" else "Wavelength (nm)", + ylabel="Delay (fs)", + unit=unit, + signed=not db, + ) + return views diff --git a/tests/gui/test_stage_preprocess.py b/tests/gui/test_stage_preprocess.py index 9bb12fc..c439a59 100644 --- a/tests/gui/test_stage_preprocess.py +++ b/tests/gui/test_stage_preprocess.py @@ -135,12 +135,14 @@ def test_stage_preprocess_on_enter_computes_once(loaded): assert loaded.status_label.text().startswith("Regridded to") loaded.on_enter() # a trace exists: re-seeds the widgets, no recompute assert loaded.state.tracedata is td - # a recompute with the view already built updates the artists in place - assert loaded._view.attached - figure_axes = list(loaded.canvas.figure.axes) + # a recompute redraws the same six maps + plots = [view.plot for view in loaded.views.values()] + assert all(plot.data is not None for plot in plots) loaded.apply(block=True) assert loaded.state.tracedata is not td - assert loaded.canvas.figure.axes == figure_axes + assert [view.plot for view in loaded.views.values()] == plots + titles = [view.title.text() for view in loaded.views.values()] + assert titles[0] == "Measured (dB)" and titles[-1] == "Regridded (lin)" def test_stage_preprocess_reset_restores_defaults(loaded): @@ -179,12 +181,6 @@ def test_stage_preprocess_fast_result_without_pending_does_not_refine(loaded): assert loaded._workers == [] -def test_stage_preprocess_replot_without_a_trace_only_attaches(loaded): - loaded.canvas.render(loaded._replot_view) - assert loaded._view.attached - assert loaded.state.tracedata is None - - def test_stage_preprocess_discard_worker_tolerates_destroyed_workers(loaded): worker = stage_preprocess.PreprocessWorker( loaded.state.preproc, loaded.state.load_data, 1, parent=loaded diff --git a/tests/gui/test_wizard_flow.py b/tests/gui/test_wizard_flow.py index 60c1fd5..332a1c0 100644 --- a/tests/gui/test_wizard_flow.py +++ b/tests/gui/test_wizard_flow.py @@ -1084,7 +1084,7 @@ def test_preprocess_interactive_change_recomputes_off_thread( stage = w.stages[2] assert state.tracedata is not None td_before = state.tracedata - assert stage._view.attached # the reusable view was built on the first draw + assert stage.views["regridded_lin"].plot.data is not None # drawn at once # Narrow the grid-λ range: schedules the 200 ms debounce, which fires the # off-thread recompute. Spinning the event loop runs both timer and worker. diff --git a/tests/test_plotting_screen.py b/tests/test_plotting_screen.py index ac29094..3b1b21e 100644 --- a/tests/test_plotting_screen.py +++ b/tests/test_plotting_screen.py @@ -209,3 +209,53 @@ def test_screen_pulse_views_tolerate_an_empty_field(): ) assert temporal.xrange is None and spectral.xrange is None assert gram.yrange is None and gram.values.max() == 0.0 + + +def test_screen_filter_views_cover_the_three_traces_in_both_scales(experiment): + from croak import preprocess + + td = preprocess.load_and_clean( + experiment.trace, + experiment.lam_nm * 1e-9, + experiment.delay_fs * 1e-15, + experiment.interaction, + lam_min=experiment.lam_min, + lam_max=experiment.lam_max, + input_unit="delay", + ) + views = screen.filter_views(td, tracedb=20.0) + assert list(views) == [key for key, *_ in screen.FILTER_PANELS] + assert views["measured_db"].levels == (-20.0, 0.0) + assert views["measured_db"].values.min() >= -20.0 + shared = plotting.filter_view_vmax(td) + assert views["filtered_lin"].levels[1] == shared + assert views["regridded_lin"].levels[1] == shared + assert views["regridded_db"].xlabel == "Frequency (PHz)" + assert views["filtered_lin"].xlabel == "Wavelength (nm)" + assert all(v.ylabel == "Delay (fs)" for v in views.values()) + + +def test_screen_simulated_views_show_the_file_density_on_a_unit_peak(): + lam = np.linspace(700e-9, 900e-9, 21) + centre = np.exp(-(((lam - 800e-9) / 30e-9) ** 2)) + trace = np.outer(centre / lam**2, np.hanning(9)) # λ-density, as loaded + data = { + "lam": lam, + "scanaxis": np.linspace(-20e-15, 20e-15, 9), + "trace": trace, + "lam_spec": lam, + "Ilam_spec": centre, + } + linear, logarithmic, delay, spectral = screen.simulated_trace_views(data) + # A unit peak before the even resampling, which on this coarse 21-sample + # axis falls between samples: within 5 %. + assert linear.levels == (0.0, 1.0) + assert linear.values.max() == pytest.approx(1.0, abs=0.05) + assert logarithmic.levels == (-screen.SIMULATED_DB, 0.0) + assert linear.ylabel == "Frequency (PHz)" and linear.yrange is not None + assert [c.name for c in spectral.curves] == ["trace marginal", "input"] + assert delay.curves[0].y.max() == pytest.approx(1.0) + blank = dict(data, trace=np.zeros_like(trace), Ilam_spec=None) + linear, _, _, spectral = screen.simulated_trace_views(blank) + assert linear.yrange is not None # a blank trace shows its full range + assert len(spectral.curves) == 1 From 6a0d419c624630e0771d841df31e80c3b97110e1 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 22:53:10 +0000 Subject: [PATCH 12/41] Draw Dispersion and Uncertainty on pyqtgraph Dispersion shows the dispersed spectrum, pulse, applied phase and spectrogram through screen views that now take a processed pulse directly (temporal_view_of, spectral_view_of, spectrogram_map_of); its PDF is drawn by matplotlib into a figure of its own. Uncertainty shows screen.uncertainty_views: the FWHM histogram with its intervals, or FWHM and trace error against thickness with the prior, and the temporal confidence band. Line views gain bars, shaded bands and spans, horizontal rules and point clouds. Co-Authored-By: Claude Opus 5.5 --- croak/gui/plots.py | 91 ++++++++- croak/gui/retrieval_views.py | 23 ++- croak/gui/stage_dispersion.py | 64 +++++-- croak/gui/stage_uncertainty.py | 39 +++- croak/plotting/screen.py | 284 ++++++++++++++++++++++++++-- tests/gui/test_retrieval_views.py | 27 +++ tests/gui/test_stage_dispersion.py | 8 +- tests/gui/test_stage_uncertainty.py | 3 +- tests/test_plotting_screen.py | 68 +++++++ 9 files changed, 564 insertions(+), 43 deletions(-) diff --git a/croak/gui/plots.py b/croak/gui/plots.py index 19c057a..eeb67c5 100644 --- a/croak/gui/plots.py +++ b/croak/gui/plots.py @@ -20,7 +20,7 @@ import numpy as np import pyqtgraph as pg from PySide6.QtCore import QPointF, QRectF, Qt -from PySide6.QtGui import QFont, QMouseEvent, QPainter +from PySide6.QtGui import QColor, QFont, QMouseEvent, QPainter from .theme import FONT_FAMILY, PALETTE, PLOT_PX @@ -422,6 +422,8 @@ class Curve: Line width, logical px. under : bool Draw beneath the other curves (a reference such as a known pulse). + points : bool + Draw the samples as dots (a scatter) instead of a line. """ x: np.ndarray @@ -432,6 +434,7 @@ class Curve: right: bool = False width: float = PEN_WIDTH under: bool = False + points: bool = False class LinePlot(Plot): @@ -455,6 +458,7 @@ def __init__( self.legend: pg.LegendItem | None = None self.right_box: pg.ViewBox | None = None self.marks: list[pg.InfiniteLine] = [] + self.extras: list[pg.GraphicsObject | pg.FillBetweenItem] = [] if right_label is not None: right = pg.ViewBox() self.right_box = right @@ -512,7 +516,18 @@ def set_curves(self, curves: Sequence[Curve], *, log_y: bool = False) -> None: style = Qt.PenStyle.DashLine if curve.dashed else Qt.PenStyle.SolidLine pen = pg.mkPen(colour, width=curve.width, style=style) x, y = self._points(curve, curve.x, curve.y) - item = pg.PlotDataItem(x, y, pen=pen) + if curve.points: + item = pg.PlotDataItem( + x, + y, + pen=None, + symbol="o", + symbolSize=5, + symbolPen=None, + symbolBrush=pg.mkColor(colour), + ) + else: + item = pg.PlotDataItem(x, y, pen=pen) item.setZValue(-1 if curve.under else 0) if curve.right and self.right_box is not None: self.right_box.addItem(item) @@ -552,6 +567,78 @@ def set_marks( self.addItem(line, ignoreBounds=True) # pyright: ignore[reportCallIssue] self.marks.append(line) + def _shade(self, colour: str, opacity: float) -> QColor: + shade = pg.mkColor(colour) + shade.setAlphaF(opacity) + return shade + + def _extra(self, item: pg.GraphicsObject | pg.FillBetweenItem, z: float) -> None: + item.setZValue(z) + self.addItem(item, ignoreBounds=True) # pyright: ignore[reportCallIssue] + self.extras.append(item) + + def clear_extras(self) -> None: + """Remove the bars, bands, spans and levels drawn with the curves.""" + for item in self.extras: + self.item.removeItem(item) + self.extras = [] + + def add_bars( + self, edges: np.ndarray, counts: np.ndarray, colour: str + ) -> pg.BarGraphItem: + """Draw a histogram: ``counts`` over the bins between ``edges``.""" + edges = np.asarray(edges, dtype=float) + bars = pg.BarGraphItem( + x0=edges[:-1], + x1=edges[1:], + height=np.asarray(counts, dtype=float), + brush=self._shade(colour, 0.7), + pen=pg.mkPen("w", width=0.6), + ) + self._extra(bars, -2) + return bars + + def add_band( + self, + x: np.ndarray, + low: np.ndarray, + high: np.ndarray, + colour: str, + opacity: float, + ) -> pg.FillBetweenItem: + """Shade between the curves ``low`` and ``high`` over ``x``.""" + lower = pg.PlotDataItem(x, low, pen=None) + upper = pg.PlotDataItem(x, high, pen=None) + band = pg.FillBetweenItem(lower, upper, brush=self._shade(colour, opacity)) + self._extra(band, -3) + return band + + def add_span( + self, + low: float, + high: float, + colour: str, + opacity: float, + *, + vertical: bool = True, + ) -> pg.LinearRegionItem: + """Shade the interval ``[low, high]`` of x (``vertical``) or of y.""" + span = pg.LinearRegionItem( + values=(low, high), + orientation="vertical" if vertical else "horizontal", + movable=False, + brush=self._shade(colour, opacity), + pen=pg.mkPen(None), + ) + self._extra(span, -4) + return span + + def add_level(self, y: float, colour: str) -> pg.InfiniteLine: + """Draw a horizontal rule at ``y``.""" + line = pg.InfiniteLine(pos=y, angle=0, pen=pg.mkPen(colour, width=1.4)) + self._extra(line, -1) + return line + def fit(self) -> None: """Fit the view to the left-axis curves, and make that the home view.""" drawn = [ diff --git a/croak/gui/retrieval_views.py b/croak/gui/retrieval_views.py index 459ca61..ebbad3a 100644 --- a/croak/gui/retrieval_views.py +++ b/croak/gui/retrieval_views.py @@ -78,6 +78,8 @@ "corrected": (COLOURS[0], True, False), "faint": ("#9cbfdd", False, False), "boundary": (PALETTE.muted, True, False), + "estimate": (COLOURS[4], False, False), + "prior": ("#9aa3ad", False, False), } REFERENCE_WIDTH = 0.9 """The width of a reference curve (the known pulse), logical px.""" @@ -134,12 +136,31 @@ def draw_line_view(plot: LinePlot, view: sc.LineView) -> None: for c in view.curves: colour, dashed, under = ROLE_STYLE[c.role] width = REFERENCE_WIDTH if under else PEN_WIDTH - curves.append(Curve(c.x, c.y, c.name, colour, dashed, c.right, width, under)) + curves.append( + Curve(c.x, c.y, c.name, colour, dashed, c.right, width, under, c.points) + ) plot.set_labels(view.xlabel, view.ylabel) if view.right_label is not None: plot.set_right_label(view.right_label) plot.set_curves(curves, log_y=view.log_y) + plot.clear_extras() + for span in view.spans: + colour = ROLE_STYLE[span.role][0] + plot.add_span(span.low, span.high, colour, span.opacity, vertical=span.vertical) + for band in view.bands: + plot.add_band( + band.x, band.low, band.high, ROLE_STYLE[band.role][0], band.opacity + ) + for y, role in view.levels: + plot.add_level(y, ROLE_STYLE[role][0]) plot.fit() + if view.bars is not None: + bars = view.bars + plot.add_bars(bars.edges, bars.counts, ROLE_STYLE[bars.role][0]) + plot.set_view( + (float(bars.edges[0]), float(bars.edges[-1])), + (0.0, 1.05 * float(np.max(bars.counts)) or 1.0), + ) if view.xrange is not None or view.yrange is not None: (x0, x1), (y0, y1) = plot.box.viewRange() xr = view.xrange or (x0, x1) diff --git a/croak/gui/stage_dispersion.py b/croak/gui/stage_dispersion.py index 8964fdb..3d41cbb 100644 --- a/croak/gui/stage_dispersion.py +++ b/croak/gui/stage_dispersion.py @@ -12,8 +12,10 @@ from dataclasses import replace import numpy as np +from matplotlib.figure import Figure from PySide6.QtWidgets import ( QFileDialog, + QGridLayout, QHBoxLayout, QLabel, QPushButton, @@ -21,10 +23,18 @@ ) from .. import dispersion, gases, materials, plotting, save +from ..plotting.screen import ( + CurveView, + LineView, + spectral_view_of, + spectrogram_map_of, + temporal_view_of, +) from ..processing import process_result, spectrogram from .base import Stage -from .canvas import MplCanvas, combo, group, with_toolbar +from .canvas import combo, group from .mirror_stack import MirrorStack +from .retrieval_views import ViewPanel from .widgets import SciSpinBox, ValueControl _GAS_NONE = "(none)" @@ -258,8 +268,21 @@ def __init__(self, state): self.controls.addWidget(btns) self.controls.addStretch(1) - self.canvas = MplCanvas(figsize=(9, 7)) - self.set_plot_area(with_toolbar(self.canvas)) + self.views = { + "spectral": ViewPanel("line", right=True), + "temporal": ViewPanel("line", right=True), + "applied": ViewPanel("line"), + "spectrogram": ViewPanel("map"), + } + grid = QGridLayout() + grid.setContentsMargins(0, 0, 0, 0) + grid.setHorizontalSpacing(14) + grid.setVerticalSpacing(10) + for index, view in enumerate(self.views.values()): + grid.addWidget(view, index // 2, index % 2) + plots = QWidget() + plots.setLayout(grid) + self.set_plot_area(plots) self.apply_help() @@ -540,20 +563,37 @@ def _applied_phase(self): ) def _draw(self, mod, pr): - self.canvas.render(lambda fig: self._plot(fig, mod, pr)) + """Show the dispersed pulse: spectrum, time, applied phase, spectrogram.""" + # Wavelength, to match the applied-phase panel beneath it. + self.views["spectral"].show_view(spectral_view_of(pr, axis="wavelength")) + self.views["temporal"].show_view(temporal_view_of(pr)) + lam_nm, phase = self._applied_curve(pr) + self.views["applied"].show_view( + LineView( + curves=(CurveView(lam_nm, phase, "retrieved", "applied"),), + title="Applied phase", + xlabel="Wavelength (nm)", + ylabel="Phase (rad)", + ) + ) + tc, lam, s = spectrogram(mod) + self.views["spectrogram"].show_view(spectrogram_map_of(tc, lam, s)) + + def _applied_curve(self, pr): + """The applied phase on the pulse's spectrum, zero at its peak.""" + phi = self._applied_phase() + phi = phi - phi[pr.mask_w][int(np.argmax(pr.Iw[pr.mask_w]))] + return pr.wavelength[pr.mask_w] / 1e-9, phi[pr.mask_w] def _plot(self, fig, mod, pr) -> None: - """Draw the four dispersion panels into ``fig``.""" + """Draw the four dispersion panels into ``fig`` (the saved PDF).""" axd = fig.subplot_mosaic("ab\ncd") # Wavelength, to match the applied-phase panel beneath it. plotting.plot_spectral(axd["a"], pr, axis="wavelength") # phase + fit plotting.plot_temporal(axd["b"], pr) # bottom-left: only the *applied* dispersion phase - phi_applied = self._applied_phase() - phi_applied = ( - phi_applied - phi_applied[pr.mask_w][int(np.argmax(pr.Iw[pr.mask_w]))] - ) - axd["c"].plot(pr.wavelength[pr.mask_w] / 1e-9, phi_applied[pr.mask_w], c="C0") + lam_nm, phase = self._applied_curve(pr) + axd["c"].plot(lam_nm, phase, c="C0") axd["c"].set_xlabel("Wavelength (nm)") axd["c"].set_ylabel("Phase (rad)") axd["c"].set_title("Applied phase") @@ -674,5 +714,7 @@ def _save(self): force=True, ) save.save_options(self.state.to_options(), os.path.join(folder, "options.toml")) - self.canvas.save_figure(os.path.join(folder, "dispersion.pdf"), dpi=600) + figure = Figure(figsize=(9, 7), layout="constrained") + self._plot(figure, mod, pr) + figure.savefig(os.path.join(folder, "dispersion.pdf"), dpi=600) self.set_status(f"Saved to {folder}") diff --git a/croak/gui/stage_uncertainty.py b/croak/gui/stage_uncertainty.py index c35bd06..a97cf5a 100644 --- a/croak/gui/stage_uncertainty.py +++ b/croak/gui/stage_uncertainty.py @@ -24,10 +24,12 @@ ) from .. import plotting, save +from ..plotting.screen import uncertainty_views from ..processing import TruthPulse -from ..uncertainty import combine_uncertainties +from ..uncertainty import UncertaintyResult, combine_uncertainties from .base import RunControls, Stage -from .canvas import MplCanvas, check, combo, dspin, group, spin, with_toolbar +from .canvas import check, combo, dspin, group, spin +from .retrieval_views import ViewPanel _METHODS = ["parametric", "delay", "frequency", "thickness", "covariance"] _NOISE_SOURCES = ["residual", "manual"] @@ -164,8 +166,19 @@ def __init__(self, state): self.controls.addStretch(1) self._sync_enabled() - self.canvas = MplCanvas(figsize=(11, 4.5)) - self.set_plot_area(with_toolbar(self.canvas)) + # Up to three panels side by side: a histogram (or FWHM and error against + # thickness) and the temporal confidence band; unused ones are hidden. + self.views = [ViewPanel("line") for _ in range(3)] + row = QHBoxLayout() + row.setContentsMargins(0, 0, 0, 0) + row.setSpacing(14) + for view in self.views: + row.addWidget(view) + view.hide() + plots = QWidget() + plots.setLayout(row) + self.set_plot_area(plots) + self._shown: UncertaintyResult | None = None self.apply_help() # -- control bindings --------------------------------------------------- @@ -294,8 +307,11 @@ def _save(self): saved = "result.h5 (+uncertainty), options.toml" # Estimates restored from a saved session may not have been plotted yet; # the PDF documents the figure on screen, so there is nothing to export. - if self.canvas.rendered: - self.canvas.save_figure(os.path.join(folder, "uncertainty.pdf"), dpi=300) + if self._shown is not None: + figure = plotting.plot_uncertainty( + self._shown, truth=_truth_fwhm(self.state.truth) + ) + figure.savefig(os.path.join(folder, "uncertainty.pdf"), dpi=300) saved += ", uncertainty.pdf" self.set_status(f"Saved {saved} to {folder}") @@ -368,7 +384,10 @@ def _on_failed(self, message): self.set_status(f"Uncertainty estimation failed: {message}") def _draw(self, uresult): - truth = _truth_fwhm(self.state.truth) - self.canvas.render( - lambda fig: plotting.plot_uncertainty(uresult, truth=truth, fig=fig) - ) + views = uncertainty_views(uresult, truth=_truth_fwhm(self.state.truth)) + for panel, view in zip(self.views, views, strict=False): + panel.show_view(view) + panel.show() + for panel in self.views[len(views) :]: + panel.hide() + self._shown = uresult diff --git a/croak/plotting/screen.py b/croak/plotting/screen.py index 138ef02..fbcc08a 100644 --- a/croak/plotting/screen.py +++ b/croak/plotting/screen.py @@ -52,9 +52,13 @@ "residual_map", "simulated_trace_views", "spectral_view", + "spectral_view_of", "spectrogram_map", + "spectrogram_map_of", "temporal_view", + "temporal_view_of", "trace_map", + "uncertainty_views", "uniform_image", ] @@ -68,6 +72,8 @@ "fit", "truth-phase", "error", + "estimate", + "prior", "predicted", "corrected", "faint", @@ -113,13 +119,50 @@ class MapView: @dataclass(frozen=True) class CurveView: - """One curve of a :class:`LineView`; ``role`` decides its colour and style.""" + """One curve of a :class:`LineView`; ``role`` decides its colour and style. + + ``points`` draws the samples as dots (a scatter) instead of a line. + """ x: NDArray[np.float64] y: NDArray[np.float64] role: str name: str = "" right: bool = False + points: bool = False + + +@dataclass(frozen=True) +class BandView: + """A shaded band between two curves over ``x`` (e.g. a confidence band).""" + + x: NDArray[np.float64] + low: NDArray[np.float64] + high: NDArray[np.float64] + role: str + name: str = "" + opacity: float = 0.25 + + +@dataclass(frozen=True) +class SpanView: + """A shaded interval across the plot: of x (``vertical``) or of y.""" + + low: float + high: float + role: str + name: str = "" + vertical: bool = True + opacity: float = 0.15 + + +@dataclass(frozen=True) +class BarsView: + """A histogram: bin ``edges`` and the ``counts`` in each bin.""" + + edges: NDArray[np.float64] + counts: NDArray[np.float64] + role: str @dataclass(frozen=True) @@ -142,6 +185,12 @@ class LineView: Short annotations, each with whether it is a warning. marks : tuple of (float, str) Vertical rules: x position and role (the role sets colour and style). + levels : tuple of (float, str) + Horizontal rules: y position and role. + bands, spans : tuple + Shaded bands between curves and shaded intervals across the plot. + bars : BarsView or None + A histogram drawn beneath the curves. """ curves: tuple[CurveView, ...] @@ -155,6 +204,10 @@ class LineView: log_y: bool = False notes: tuple[tuple[str, bool], ...] = field(default_factory=tuple) marks: tuple[tuple[float, str], ...] = field(default_factory=tuple) + levels: tuple[tuple[float, str], ...] = field(default_factory=tuple) + bands: tuple[BandView, ...] = field(default_factory=tuple) + spans: tuple[SpanView, ...] = field(default_factory=tuple) + bars: BarsView | None = None MAX_SAMPLES = 2048 @@ -321,7 +374,30 @@ def residual_map(data: RetrievalPlotData) -> MapView | None: def spectrogram_map(data: RetrievalPlotData) -> MapView: """Return the retrieved pulse's Gabor spectrogram, time against wavelength.""" t_centres, wavelength, s = data.spectrogram - x, y, values = uniform_image(t_centres / 1e-15, wavelength / 1e-9, s) + return spectrogram_map_of( + t_centres, + wavelength, + s, + window=_delay_window(data), + band=(data.lam_min, data.lam_max), + ) + + +def spectrogram_map_of( + t_centres: ArrayLike, + wavelength: ArrayLike, + s: ArrayLike, + *, + window: tuple[float, float] | None = None, + band: tuple[float, float] | None = None, +) -> MapView: + """Return a spectrogram ``(t, λ, S)`` (s, m) as a map, time against wavelength. + + ``window`` is the time window shown (fs) and ``band`` the wavelength band (m). + """ + x, y, values = uniform_image( + np.asarray(t_centres) / 1e-15, np.asarray(wavelength) / 1e-9, s + ) return MapView( values=values, x=x, @@ -331,23 +407,48 @@ def spectrogram_map(data: RetrievalPlotData) -> MapView: xlabel="Time (fs)", ylabel="Wavelength (nm)", unit="a.u.", - xrange=_delay_window(data), - yrange=(data.lam_min / 1e-9, data.lam_max / 1e-9), + xrange=window, + yrange=None if band is None else (band[0] / 1e-9, band[1] / 1e-9), ) def temporal_view(data: RetrievalPlotData) -> LineView: """Return the retrieved pulse: intensity or power, transform limit, phase.""" - pr = data.processed - retr, tl, r_label, tl_label, ylabel, power_scale = _temporal_curves( - pr, data.power_scale + return temporal_view_of( + data.processed, + power_scale=data.power_scale, + truth=data.truth, + window=_delay_window(data), ) + + +def temporal_view_of( + pr, + *, + power_scale: tuple[float, str] | None = None, + truth=None, + window: tuple[float, float] | None = None, +) -> LineView: + """Return a processed pulse in time: intensity or power, transform limit, phase. + + Parameters + ---------- + pr : ProcessedResult + The pulse (:func:`croak.processing.process_result`). + power_scale : tuple of (float, str), optional + The SI ``(scale, unit)`` of an absolute power axis (derived when the + pulse energy is known and this is None). + truth : TruthPulse, optional + A known pulse to draw beneath it. + window : tuple of float, optional + The time window shown (fs). + """ + retr, tl, r_label, tl_label, ylabel, power_scale = _temporal_curves(pr, power_scale) curves = [ CurveView(pr.t_over / 1e-15, retr, "retrieved", r_label), CurveView(pr.t_tl / 1e-15, tl, "transform-limited", tl_label), ] truth_curve = None - truth = data.truth if truth is not None: truth_curve = _truth_intensity(truth, pr.energy, power_scale) curves.append( @@ -379,7 +480,7 @@ def temporal_view(data: RetrievalPlotData) -> LineView: xlabel="Time (fs)", ylabel=ylabel, right_label="Phase (rad)", - xrange=_delay_window(data), + xrange=window, yrange=_intensity_ylim(retr, tl, truth_curve), right_range=_phase_ylim(pr.phi_t[pr.mask_t], truth_phase), ) @@ -391,31 +492,58 @@ def spectral_view(data: RetrievalPlotData) -> LineView: Its notes are the GDD/TOD of the phase fit and the energy at the grid's edge, a warning above :data:`~croak.plotting.EDGE_ENERGY_WARN`. """ - pr = data.processed - axis = data.spectral_axis + return spectral_view_of( + data.processed, + axis=data.spectral_axis, + band=(data.lam_min, data.lam_max), + truth=data.truth, + ) + + +def spectral_view_of( + pr, + *, + axis: str = "frequency", + band: tuple[float, float] | None = None, + truth=None, +) -> LineView: + """Return a processed pulse's spectrum on ``axis``, with its phase and fit. + + Parameters + ---------- + pr : ProcessedResult + The pulse. + axis : {"frequency", "wavelength"} + The abscissa. + band : tuple of float, optional + The wavelength band shown (m), on either axis. + truth : TruthPulse, optional + A known pulse to draw beneath it. + """ x, retrieved, measured, xlabel = _spectral_curves(pr, axis) curves = [CurveView(x, retrieved, "retrieved", "R")] if measured is not None: curves.append(CurveView(x, measured, "measured", "M")) - truth = None if data.truth is None else _truth_spectrum(data.truth, axis) - if truth is not None: - curves.append(CurveView(truth[0], truth[1], "truth", "truth")) + known = None if truth is None else _truth_spectrum(truth, axis) + if known is not None: + curves.append(CurveView(known[0], known[1], "truth", "truth")) mask = pr.mask_w curves.append(CurveView(x[mask], pr.phi_w[mask], "phase", "phase", True)) curves.append(CurveView(x[mask], pr.phi_w_fit[mask], "fit", "phase fit", True)) - truth_phase = _truth_spectral_phase(pr.wavelength, data.truth) + truth_phase = _truth_spectral_phase(pr.wavelength, truth) if truth_phase is not None: curves.append( CurveView(x[mask], truth_phase[mask], "truth-phase", "truth phase", True) ) edge = pr.edge_energy + lam_min, lam_max = band if band is not None else (None, None) return LineView( curves=tuple(curves), title="Spectrum", xlabel=xlabel, ylabel="Power (a.u.)", right_label="Phase (rad)", - xrange=_spectral_xlim(data.lam_min, data.lam_max, axis), + xrange=_spectral_xlim(lam_min, lam_max, axis), yrange=(0.0, 1.05), right_range=_phase_ylim( pr.phi_w[mask], None if truth_phase is None else truth_phase[mask] @@ -860,3 +988,127 @@ def filter_views(td, *, tracedb: float = 30.0) -> dict[str, MapView]: signed=not db, ) return views + + +def _histogram_bins(count: int) -> int: + """Return the bin count: a fifth of the samples, from 12 to 40.""" + return int(np.clip(count // 5, 12, 40)) + + +def _fwhm_distribution_view(uresult, truth: float | None) -> LineView: + samples = np.asarray(uresult.samples, dtype=float) / 1e-15 + counts, edges = np.histogram(samples, bins=_histogram_bins(samples.size)) + lo95, hi95 = (v / 1e-15 for v in uresult.interval_95) + lo68, hi68 = (v / 1e-15 for v in uresult.interval_68) + marks = [(uresult.point_estimate / 1e-15, "estimate")] + if truth is not None: + marks.append((truth / 1e-15, "truth-phase")) + return LineView( + curves=(), + title=( + f"{uresult.point_estimate / 1e-15:.2f} ± " + f"{uresult.plus_minus / 1e-15:.2f} fs (B={uresult.n_converged})" + ), + xlabel="FWHM (fs)", + ylabel="Replicates", + spans=( + SpanView(lo95, hi95, "retrieved", "95 %", opacity=0.10), + SpanView(lo68, hi68, "retrieved", "68 %", opacity=0.20), + ), + bars=BarsView(edges, counts.astype(float), "retrieved"), + marks=tuple(marks), + yrange=(0.0, 1.05 * float(counts.max())), + ) + + +def _intensity_band_view(uresult) -> LineView: + t = np.asarray(uresult.t_profile, dtype=float) / 1e-15 + profiles = np.asarray(uresult.profiles, dtype=float) + median = np.median(profiles, axis=0) + lo68, hi68 = np.percentile(profiles, [16.0, 84.0], axis=0) + lo95, hi95 = np.percentile(profiles, [2.5, 97.5], axis=0) + support = t[median > 0.01 * median.max()] + window = None + if support.size: + pad = PULSE_MARGIN * (support.max() - support.min() + 1.0) + window = (float(support.min() - pad), float(support.max() + pad)) + return LineView( + curves=(CurveView(t, median, "retrieved", "median"),), + title="Temporal intensity", + xlabel="Time (fs)", + ylabel="Intensity (a.u.)", + bands=( + BandView(t, lo95, hi95, "retrieved", "95 %", opacity=0.15), + BandView(t, lo68, hi68, "retrieved", "68 %", opacity=0.30), + ), + xrange=window, + yrange=(0.0, 1.05 * float(np.max(hi95))), + ) + + +def _thickness_prior(uresult) -> tuple[SpanView, ...]: + if uresult.thickness_central is None or uresult.thickness_sigma is None: + return () + central = uresult.thickness_central / 1e-6 + sigma = uresult.thickness_sigma / 1e-6 + return (SpanView(central - sigma, central + sigma, "prior", "L₀ ± σ_L"),) + + +def _fwhm_vs_thickness_view(uresult, truth: float | None) -> LineView: + thick = np.asarray(uresult.thicknesses, dtype=float) / 1e-6 + fwhm = np.asarray(uresult.samples, dtype=float) / 1e-15 + lo68, hi68 = (v / 1e-15 for v in uresult.interval_68) + levels = [(uresult.point_estimate / 1e-15, "estimate")] + if truth is not None: + levels.append((truth / 1e-15, "truth-phase")) + return LineView( + curves=(CurveView(thick, fwhm, "retrieved", "draws", points=True),), + title="FWHM vs thickness", + xlabel="Substrate thickness (µm)", + ylabel="FWHM (fs)", + spans=( + *_thickness_prior(uresult), + SpanView(lo68, hi68, "estimate", "68 %", vertical=False), + ), + levels=tuple(levels), + ) + + +def _error_vs_thickness_view(uresult) -> LineView: + thick = np.asarray(uresult.thicknesses, dtype=float) / 1e-6 + errors = np.asarray(uresult.frog_errors, dtype=float) + return LineView( + curves=(CurveView(thick, errors, "measured", "draws", points=True),), + title="Trace misfit vs thickness", + xlabel="Substrate thickness (µm)", + ylabel="FROG error R", + spans=_thickness_prior(uresult), + ) + + +def uncertainty_views(uresult, *, truth: float | None = None) -> list[LineView]: + """Return an uncertainty estimate's views, as the composite figure lays them out. + + A resampling estimate gives the FWHM histogram with its 68 % and 95 % + intervals; a substrate-thickness estimate gives FWHM and FROG error against + the drawn thickness, with the thickness prior. Either is followed by the + temporal intensity's pointwise confidence band when profiles were + collected (see :func:`croak.plotting.plot_uncertainty`). + + Parameters + ---------- + uresult : UncertaintyResult + The estimate. + truth : float, optional + A known true FWHM (s) to mark. + """ + if getattr(uresult, "thicknesses", None) is not None: + views = [ + _fwhm_vs_thickness_view(uresult, truth), + _error_vs_thickness_view(uresult), + ] + else: + views = [_fwhm_distribution_view(uresult, truth)] + if uresult.profiles is not None and uresult.t_profile is not None: + views.append(_intensity_band_view(uresult)) + return views diff --git a/tests/gui/test_retrieval_views.py b/tests/gui/test_retrieval_views.py index c558ee9..cce9716 100644 --- a/tests/gui/test_retrieval_views.py +++ b/tests/gui/test_retrieval_views.py @@ -171,3 +171,30 @@ def test_result_page_exports_its_figure(qtbot, data, tmp_path, monkeypatch): qtbot.addWidget(nothing) assert nothing.export_figure() == "" assert "Run a retrieval first" in messages[-1] + + +def test_view_panel_draws_bars_bands_spans_points_and_levels(qtbot): + from test_plotting_screen import _estimate + + from croak.gui.retrieval_views import ViewPanel + from croak.plotting.screen import uncertainty_views + + panels = [ViewPanel("line") for _ in range(3)] + for panel in panels: + qtbot.addWidget(panel) + hist, band = uncertainty_views(_estimate()) + panels[0].show_view(hist) + panels[1].show_view(band) + plot = panels[0].plot + kinds = {type(item).__name__ for item in plot.extras} + assert kinds == {"BarGraphItem", "LinearRegionItem"} + assert plot.box.viewRange()[0] == pytest.approx( + [hist.bars.edges[0], hist.bars.edges[-1]] + ) + assert any(type(i).__name__ == "FillBetweenItem" for i in panels[1].plot.extras) + fwhm, error, _ = uncertainty_views(_estimate("thickness", thickness=True)) + panels[0].show_view(fwhm) # redrawn: the histogram's extras go + kinds = {type(item).__name__ for item in plot.extras} + assert "BarGraphItem" not in kinds and "InfiniteLine" in kinds + assert plot.items[0].opts["symbol"] == "o" + panels[2].show_view(error) diff --git a/tests/gui/test_stage_dispersion.py b/tests/gui/test_stage_dispersion.py index 1d72a10..db82d20 100644 --- a/tests/gui/test_stage_dispersion.py +++ b/tests/gui/test_stage_dispersion.py @@ -90,7 +90,7 @@ def test_without_a_result_everything_is_inert(qtbot): lambda: stage._auto_row(stage.mirror_stack.add_row()), ): action() - assert not stage.canvas.rendered + assert stage.views["spectrogram"].plot.data is None # nothing drawn with pytest.raises(RuntimeError, match="no retrieval result"): stage._modified() with pytest.raises(RuntimeError, match="no retrieval result"): @@ -113,7 +113,11 @@ def test_base_without_a_result_is_inert_for_the_tuners(qtbot, chirped_result): def test_entering_shows_the_pulse_and_power_labels(stage): - assert stage.canvas.rendered + titles = [view.title.text() for view in stage.views.values()] + assert titles == ["Spectrum", "Retrieved pulse", "Applied phase", "Spectrogram"] + assert stage.views["spectral"].plot.item.getAxis("bottom").labelText == ( + "Wavelength (nm)" + ) assert stage.fwhm_label.text().endswith(" fs") assert stage.peak_label.text() == "–" stage.energy.setValue(1e-6) diff --git a/tests/gui/test_stage_uncertainty.py b/tests/gui/test_stage_uncertainty.py index 35552e5..b69b7fe 100644 --- a/tests/gui/test_stage_uncertainty.py +++ b/tests/gui/test_stage_uncertainty.py @@ -112,7 +112,8 @@ def test_on_enter_lists_and_draws_the_current_estimate(stage, retrieval): stage.state.set_uncertainty_result(_uresult("parametric", 0.05e-15)) stage.on_enter() assert stage.est_list.count() == 1 - assert stage.canvas.rendered + shown = [view.title.text() for view in stage.views if not view.isHidden()] + assert shown[1] == "Temporal intensity" and len(shown) == 2 assert stage.status_label.text() == stage.state.uncertainty_result.summary() diff --git a/tests/test_plotting_screen.py b/tests/test_plotting_screen.py index 3b1b21e..64a718c 100644 --- a/tests/test_plotting_screen.py +++ b/tests/test_plotting_screen.py @@ -259,3 +259,71 @@ def test_screen_simulated_views_show_the_file_density_on_a_unit_peak(): linear, _, _, spectral = screen.simulated_trace_views(blank) assert linear.yrange is not None # a blank trace shows its full range assert len(spectral.curves) == 1 + + +def _estimate(method="parametric", *, profiles=True, thickness=False): + from croak.uncertainty import UncertaintyResult + + rng = np.random.default_rng(1) + t = np.linspace(-15e-15, 15e-15, 30) + extra = {} + if thickness: + extra = { + "thicknesses": rng.normal(10e-6, 1e-6, 40), + "thickness_central": 10e-6, + "thickness_sigma": 1e-6, + } + return UncertaintyResult( + statistic="fwhm", + point_estimate=5e-15, + samples=5e-15 + 0.1e-15 * rng.standard_normal(40), + interval_68=(4.9e-15, 5.1e-15), + interval_95=(4.8e-15, 5.2e-15), + interval_method="percentile", + method=method, + n_resamples=40, + n_converged=40, + frog_errors=rng.uniform(0.01, 0.02, 40), + base_error=0.01, + profiles=( + np.exp(-((t / 3e-15) ** 2)) * rng.uniform(0.9, 1.1, (40, 1)) + if profiles + else None + ), + t_profile=t if profiles else None, + **extra, + ) + + +def test_screen_uncertainty_views_follow_the_composite_figure(): + hist, band = screen.uncertainty_views(_estimate(), truth=5.05e-15) + assert hist.bars is not None and hist.bars.counts.sum() == 40 + assert len(hist.bars.edges) == 12 + 1 # a fifth of 40, at least 12 bins + assert [s.name for s in hist.spans] == ["95 %", "68 %"] + assert [role for _, role in hist.marks] == ["estimate", "truth-phase"] + assert hist.title.startswith("5.00 ± ") + assert [b.name for b in band.bands] == ["95 %", "68 %"] + assert band.xrange is not None and band.xrange[0] < 0 < band.xrange[1] + (only,) = screen.uncertainty_views(_estimate(profiles=False)) + assert only.marks == ((5.0, "estimate"),) + + +def test_screen_uncertainty_views_for_the_thickness_systematic(): + fwhm, error, band = screen.uncertainty_views( + _estimate("thickness", thickness=True), truth=5e-15 + ) + assert fwhm.curves[0].points and error.curves[0].points + assert [s.name for s in fwhm.spans] == ["L₀ ± σ_L", "68 %"] + assert not fwhm.spans[1].vertical + assert [role for _, role in fwhm.levels] == ["estimate", "truth-phase"] + assert [s.name for s in error.spans] == ["L₀ ± σ_L"] + assert band.title == "Temporal intensity" + no_prior = replace(_estimate("thickness", thickness=True), thickness_sigma=None) + fwhm, error, _ = screen.uncertainty_views(no_prior) + assert [s.name for s in fwhm.spans] == ["68 %"] and error.spans == () + + +def test_screen_uncertainty_band_of_nothing_has_no_window(): + flat = replace(_estimate(), profiles=np.zeros((40, 30))) + _, band = screen.uncertainty_views(flat) + assert band.xrange is None From fe24e6b1a230d03dd2010de05243d252730599fb Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 22:59:42 +0000 Subject: [PATCH 13/41] Remove the matplotlib canvas and live-preview helpers Nothing on screen uses matplotlib now. MplCanvas and with_toolbar go, and croak.gui.canvas becomes croak.gui.controls, the form builders it still held. In croak.plotting, RetrievalPageView, FrogFilterView, the update_* panel functions, PlotScale/plot_scale/scaled_rc and PanelSpec.update go; the drawn figures are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../gui-matplotlib-live-preview.removed.md | 1 + croak/gui/base.py | 2 +- croak/gui/canvas.py | 307 ---------- croak/gui/controls.py | 152 +++++ croak/gui/mirror_stack.py | 2 +- croak/gui/stage_dispersion.py | 2 +- croak/gui/stage_load.py | 2 +- croak/gui/stage_marginal_check.py | 2 +- croak/gui/stage_preprocess.py | 2 +- croak/gui/stage_retrieve.py | 2 +- croak/gui/stage_simulated.py | 2 +- croak/gui/stage_synthetic.py | 2 +- croak/gui/stage_uncertainty.py | 2 +- croak/plotting/__init__.py | 67 +-- croak/plotting/retrieval.py | 159 +----- croak/plotting/scale.py | 152 ----- croak/plotting/traces.py | 173 ------ croak/plotting/updates.py | 240 -------- tests/gui/test_base.py | 2 +- tests/gui/test_canvas.py | 80 --- tests/gui/test_controls.py | 30 + tests/gui/test_wizard_flow.py | 57 +- tests/test_plotting.py | 533 ++---------------- tests/test_plotting_screen.py | 18 + 24 files changed, 274 insertions(+), 1717 deletions(-) create mode 100644 changelog.d/gui-matplotlib-live-preview.removed.md delete mode 100644 croak/gui/canvas.py create mode 100644 croak/gui/controls.py delete mode 100644 croak/plotting/scale.py delete mode 100644 croak/plotting/updates.py delete mode 100644 tests/gui/test_canvas.py create mode 100644 tests/gui/test_controls.py diff --git a/changelog.d/gui-matplotlib-live-preview.removed.md b/changelog.d/gui-matplotlib-live-preview.removed.md new file mode 100644 index 0000000..3693026 --- /dev/null +++ b/changelog.d/gui-matplotlib-live-preview.removed.md @@ -0,0 +1 @@ +The matplotlib helpers that only the old GUI's live preview used are gone: `croak.plotting.RetrievalPageView`, `FrogFilterView`, the `update_*` panel functions (and `update_convergence_curve`), `PlotScale`, `plot_scale`, `scaled_rc` and their constants, and `PanelSpec.update`. The GUI draws on screen with pyqtgraph from the new `croak.plotting.screen` view data; the `draw_*` panels, `plot_retrieval`, `plot_retrieval_page`, `plot_frog_filter` and the other figures are unchanged. diff --git a/croak/gui/base.py b/croak/gui/base.py index cca6fcf..7f5c514 100644 --- a/croak/gui/base.py +++ b/croak/gui/base.py @@ -21,7 +21,7 @@ class Stage(StagePage): """A stage page: build into :attr:`controls`, then :meth:`set_plot_area`. - Subclasses add their sections (:func:`croak.gui.canvas.group`) to + Subclasses add their sections (:func:`croak.gui.controls.group`) to ``self.controls``, hand their plots to :meth:`set_plot_area`, call :meth:`apply_help` at the end of ``__init__`` and implement :meth:`on_enter`. diff --git a/croak/gui/canvas.py b/croak/gui/canvas.py deleted file mode 100644 index 037ba1d..0000000 --- a/croak/gui/canvas.py +++ /dev/null @@ -1,307 +0,0 @@ -"""Matplotlib canvas embedded in Qt, plus small widget helpers.""" - -from __future__ import annotations - -from collections.abc import Callable -from pathlib import Path - -import matplotlib -from matplotlib.backends.backend_qt import NavigationToolbar2QT -from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg -from matplotlib.figure import Figure -from PySide6.QtCore import QTimer -from PySide6.QtGui import QResizeEvent -from PySide6.QtWidgets import ( - QCheckBox, - QComboBox, - QDoubleSpinBox, - QFormLayout, - QSpinBox, - QVBoxLayout, - QWidget, -) -from superqt import QCollapsible - -from ..plotting import FULL_SCALE, PlotScale, plot_scale, scaled_rc -from . import settings -from .sheet import Section - -__all__ = [ - "MplCanvas", - "with_toolbar", - "collapsible", - "group", - "add_wide_row", - "spin", - "dspin", - "combo", - "check", -] - - -class MplCanvas(FigureCanvasQTAgg): - """A matplotlib canvas whose text density follows its on-screen size. - - ``figsize`` is the size in inches the figure's layout was *designed* for: the - canvas prefers it, but Qt resizes the figure freely to fill its slot. Text is - sized in points, so in a small window the axes would shrink while the text did - not (on a 1366x768 laptop the twelve-panel retrieval figure spent 78 % of its - area on text and padding). :meth:`render` therefore plots inside a - :func:`matplotlib.rc_context` scaled by :func:`croak.plotting.plot_scale` - (down to 70 % of the default sizes), and a resize that crosses a density step - re-runs the remembered plot function after a short debounce. - - Parameters - ---------- - figsize : tuple of float - Design size ``(width, height)`` in inches; also the initial figure size. - """ - - #: Debounce (ms) between a resize and the re-plot: a window drag fires many - #: resize events, and one redraw once it settles is enough. - REPLOT_DELAY_MS = 150 - - def __init__(self, figsize: tuple[float, float] = (5.0, 4.0)): - self.design_size: tuple[float, float] = figsize - figure = Figure(figsize=figsize, layout="constrained") - super().__init__(figure) - self._plot_fn: Callable[[Figure], object] | None = None - self._drawn_scale: PlotScale | None = None - self._replot_timer = QTimer(self) - self._replot_timer.setSingleShot(True) - self._replot_timer.setInterval(self.REPLOT_DELAY_MS) - self._replot_timer.timeout.connect(self._replot) - - @property - def rendered(self) -> bool: - """Whether :meth:`render` has been called (so there is a plot to export).""" - return self._plot_fn is not None - - @property - def scale(self) -> PlotScale: - """Text density for the canvas's current on-screen size.""" - width, height = self.figure.get_size_inches() - return plot_scale(float(width), float(height), self.design_size) - - def render(self, plot_fn: Callable[[Figure], object]) -> None: - """Clear the figure, run ``plot_fn(figure)`` at the current density, redraw. - - ``plot_fn`` is remembered and re-run whenever a resize changes the density, - so it must read current state rather than values that go stale, and it - must tolerate being called again. Matplotlib resolves font sizes when an - artist is *created*, so everything that creates text has to happen inside - ``plot_fn``; call :meth:`draw_idle` directly only for in-place data updates - that create no new artists (see :meth:`croak.plotting.FrogFilterView.update`). - - Parameters - ---------- - plot_fn : callable - Receives the cleared :class:`~matplotlib.figure.Figure` and draws into - it. Its return value is ignored. - """ - self._plot_fn = plot_fn - self._draw_at(self.scale) - - def clear(self) -> None: - """Blank the figure and forget the plot function (nothing to re-plot).""" - self._plot_fn = None - self._drawn_scale = None - self.figure.clear() - self.draw_idle() - - def draw(self) -> None: - """Render while keeping the owning Qt window alive.""" - # A queued draw retains the Python canvas but not its owning window. - # If cyclic collection destroys that window during rendering, Qt also - # deletes the canvas before matplotlib updates it/restores its cursor. - # Retain the owner only for this call, without creating a permanent cycle. - _window = self.window() - super().draw() - - def save_figure(self, path: str | Path, *, dpi: int) -> None: - """Save the remembered plot at the design size and full text density. - - The on-screen figure may be small and drawn with shrunken text; the export - is drawn afresh into a headless figure of :attr:`design_size` inches at - matplotlib's default sizes, so a PDF saved from a laptop window is the same - file a large display would produce. - - Parameters - ---------- - path : str or Path - Output file; the format follows the extension (``.pdf``, ``.png``, …). - dpi : int - Raster resolution for image content (the pcolormesh panels are - rasterised even inside a PDF). - - Raises - ------ - RuntimeError - If nothing has been rendered on this canvas yet. - """ - if self._plot_fn is None: - raise RuntimeError("nothing has been rendered on this canvas yet") - figure = Figure(figsize=self.design_size, layout="constrained") - with matplotlib.rc_context(scaled_rc(FULL_SCALE)): - self._plot_fn(figure) - figure.savefig(path, dpi=dpi) - - def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802 — Qt override - """Let matplotlib resize the figure, then schedule a re-plot if needed.""" - super().resizeEvent(event) - if self._plot_fn is not None and self.scale != self._drawn_scale: - self._replot_timer.start() - - def _draw_at(self, scale: PlotScale) -> None: - """Run the remembered plot function at ``scale`` and repaint.""" - if self._plot_fn is None: - raise RuntimeError("no plot function to draw; call render() first") - self._drawn_scale = scale - with matplotlib.rc_context(scaled_rc(scale)): - self.figure.clear() - self._plot_fn(self.figure) - self.draw_idle() - - def _replot(self) -> None: - """Debounced resize handler: re-plot only if the density really changed.""" - if self._plot_fn is not None and self.scale != self._drawn_scale: - self._draw_at(self.scale) - - -def with_toolbar(canvas: MplCanvas) -> QWidget: - """Wrap ``canvas`` in a panel with a matplotlib pan/zoom/save toolbar on top. - - Returns a container widget to drop into a layout; the caller keeps its own - reference to ``canvas`` for plotting. The toolbar adds the standard - Home/Back/Forward, Pan, Zoom-to-rectangle, Subplots, and Save controls, plus a - live cursor read-out. A full redraw (:meth:`MplCanvas.render`) resets the - view, so zoom is most useful between control changes; Home restores it. - """ - panel = QWidget() - layout = QVBoxLayout(panel) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(NavigationToolbar2QT(canvas, panel)) - layout.addWidget(canvas) - return panel - - -def collapsible( - title: str, - content: QWidget, - *, - expanded: bool = True, - settings_key: str | None = None, -) -> QCollapsible: - """Wrap ``content`` in a click-to-fold strip headed ``title``. - - For auxiliary widgets that sit under a plot (a numeric read-out, say) and - have a fixed natural height: folding one hands its pixels back to the plot. - Not for canvases, which should stretch — put those on a splitter instead. - - Parameters - ---------- - title : str - Header text; clicking it toggles the strip. - content : QWidget - The widget to fold. - expanded : bool - Initial state when nothing is remembered. - settings_key : str, optional - With a key, the state is restored from :mod:`croak.gui.settings` and - every toggle (by click or by :meth:`~superqt.QCollapsible.collapse` / - :meth:`~superqt.QCollapsible.expand`) is remembered under it. - - Returns - ------- - superqt.QCollapsible - """ - strip = QCollapsible(title) - strip.setContent(content) - strip.setDuration(120) # visible but brief - if settings_key is not None: - expanded = settings.value(settings_key, expanded) - if expanded: - strip.expand(animate=False) - else: - strip.collapse(animate=False) - if settings_key is not None: - key = settings_key - strip.toggled.connect(lambda state: settings.set_value(key, state)) - return strip - - -# -- tiny declarative widget builders --------------------------------------- -def group( - title: str, *, folded: bool = False, settings_key: str = "" -) -> tuple[Section, QFormLayout]: - """A folding section and the form layout its rows go in. - - The section is added to a stage's ``controls``; its rows are added to the - form with ``form.addRow(label, widget)``. Help tooltips match rows by - ``(title, label)`` (:func:`croak.gui.help.apply_tooltips`). ``folded`` and - ``settings_key`` are as for :class:`~croak.gui.sheet.Section`. - """ - section = Section(title, folded=folded, settings_key=settings_key) - return section, section.form - - -def add_wide_row(form: QFormLayout, label: str, widget: QWidget) -> None: - """Add a form row laid out label-above-field, so the field spans the column. - - Used for the slider controls (:class:`~croak.gui.widgets.RangeControl` / - :class:`~croak.gui.widgets.ValueControl`), which are unusable when squeezed into - the narrow right-hand field column. ``WrapAllRows`` is a *form*-wide policy, so - adding one slider wraps the rest of its group box too — that is the intended - trade and keeps a group visually uniform. The label stays in the form's label - role (rather than becoming a separate spanning row), which is what - :func:`~croak.gui.help.apply_tooltips` matches help entries against. - """ - form.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapAllRows) - form.addRow(label, widget) - - -def spin(lo: int, hi: int, value: int, on_change=None, step: int = 1) -> QSpinBox: - w = QSpinBox() - w.setRange(lo, hi) - w.setSingleStep(step) - w.setValue(int(value)) - if on_change: - w.valueChanged.connect(on_change) - return w - - -def dspin( - lo: float, - hi: float, - value: float, - on_change=None, - decimals: int = 3, - step: float = 1.0, -) -> QDoubleSpinBox: - w = QDoubleSpinBox() - w.setRange(lo, hi) - w.setDecimals(decimals) - w.setSingleStep(step) - w.setValue(float(value)) - if on_change: - w.valueChanged.connect(on_change) - return w - - -def combo(items, value=None, on_change=None) -> QComboBox: - w = QComboBox() - w.addItems([str(i) for i in items]) - if value is not None and str(value) in [str(i) for i in items]: - w.setCurrentText(str(value)) - if on_change: - w.currentTextChanged.connect(on_change) - return w - - -def check(label: str, value: bool, on_change=None) -> QCheckBox: - w = QCheckBox(label) - w.setChecked(bool(value)) - if on_change: - w.toggled.connect(on_change) - return w diff --git a/croak/gui/controls.py b/croak/gui/controls.py new file mode 100644 index 0000000..2e58b13 --- /dev/null +++ b/croak/gui/controls.py @@ -0,0 +1,152 @@ +"""Small builders for a stage's form controls. + +:func:`group` makes a folding section and the form its rows go in; the others +build a spin box, combo or check box already bound to a callback, and +:func:`collapsible` folds a strip (the retrieval read-out) under the plots. +""" + +from __future__ import annotations + +from PySide6.QtWidgets import ( + QCheckBox, + QComboBox, + QDoubleSpinBox, + QFormLayout, + QSpinBox, + QWidget, +) +from superqt import QCollapsible + +from . import settings +from .sheet import Section + +__all__ = [ + "collapsible", + "group", + "add_wide_row", + "spin", + "dspin", + "combo", + "check", +] + + +def collapsible( + title: str, + content: QWidget, + *, + expanded: bool = True, + settings_key: str | None = None, +) -> QCollapsible: + """Wrap ``content`` in a click-to-fold strip headed ``title``. + + For auxiliary widgets that sit under a plot (a numeric read-out, say) and + have a fixed natural height: folding one hands its pixels back to the plot. + Not for canvases, which should stretch — put those on a splitter instead. + + Parameters + ---------- + title : str + Header text; clicking it toggles the strip. + content : QWidget + The widget to fold. + expanded : bool + Initial state when nothing is remembered. + settings_key : str, optional + With a key, the state is restored from :mod:`croak.gui.settings` and + every toggle (by click or by :meth:`~superqt.QCollapsible.collapse` / + :meth:`~superqt.QCollapsible.expand`) is remembered under it. + + Returns + ------- + superqt.QCollapsible + """ + strip = QCollapsible(title) + strip.setContent(content) + strip.setDuration(120) # visible but brief + if settings_key is not None: + expanded = settings.value(settings_key, expanded) + if expanded: + strip.expand(animate=False) + else: + strip.collapse(animate=False) + if settings_key is not None: + key = settings_key + strip.toggled.connect(lambda state: settings.set_value(key, state)) + return strip + + +# -- tiny declarative widget builders --------------------------------------- +def group( + title: str, *, folded: bool = False, settings_key: str = "" +) -> tuple[Section, QFormLayout]: + """A folding section and the form layout its rows go in. + + The section is added to a stage's ``controls``; its rows are added to the + form with ``form.addRow(label, widget)``. Help tooltips match rows by + ``(title, label)`` (:func:`croak.gui.help.apply_tooltips`). ``folded`` and + ``settings_key`` are as for :class:`~croak.gui.sheet.Section`. + """ + section = Section(title, folded=folded, settings_key=settings_key) + return section, section.form + + +def add_wide_row(form: QFormLayout, label: str, widget: QWidget) -> None: + """Add a form row laid out label-above-field, so the field spans the column. + + Used for the slider controls (:class:`~croak.gui.widgets.RangeControl` / + :class:`~croak.gui.widgets.ValueControl`), which are unusable when squeezed into + the narrow right-hand field column. ``WrapAllRows`` is a *form*-wide policy, so + adding one slider wraps the rest of its group box too — that is the intended + trade and keeps a group visually uniform. The label stays in the form's label + role (rather than becoming a separate spanning row), which is what + :func:`~croak.gui.help.apply_tooltips` matches help entries against. + """ + form.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapAllRows) + form.addRow(label, widget) + + +def spin(lo: int, hi: int, value: int, on_change=None, step: int = 1) -> QSpinBox: + w = QSpinBox() + w.setRange(lo, hi) + w.setSingleStep(step) + w.setValue(int(value)) + if on_change: + w.valueChanged.connect(on_change) + return w + + +def dspin( + lo: float, + hi: float, + value: float, + on_change=None, + decimals: int = 3, + step: float = 1.0, +) -> QDoubleSpinBox: + w = QDoubleSpinBox() + w.setRange(lo, hi) + w.setDecimals(decimals) + w.setSingleStep(step) + w.setValue(float(value)) + if on_change: + w.valueChanged.connect(on_change) + return w + + +def combo(items, value=None, on_change=None) -> QComboBox: + w = QComboBox() + w.addItems([str(i) for i in items]) + if value is not None and str(value) in [str(i) for i in items]: + w.setCurrentText(str(value)) + if on_change: + w.currentTextChanged.connect(on_change) + return w + + +def check(label: str, value: bool, on_change=None) -> QCheckBox: + w = QCheckBox(label) + w.setChecked(bool(value)) + if on_change: + w.toggled.connect(on_change) + return w diff --git a/croak/gui/mirror_stack.py b/croak/gui/mirror_stack.py index 63727a9..ec93b04 100644 --- a/croak/gui/mirror_stack.py +++ b/croak/gui/mirror_stack.py @@ -32,7 +32,7 @@ from .. import dispersion, io, mirrors from ..session.params import BeamPathMirror -from .canvas import combo, spin +from .controls import combo, spin __all__ = ["MirrorRow", "MirrorStack"] diff --git a/croak/gui/stage_dispersion.py b/croak/gui/stage_dispersion.py index 3d41cbb..b86a13f 100644 --- a/croak/gui/stage_dispersion.py +++ b/croak/gui/stage_dispersion.py @@ -32,7 +32,7 @@ ) from ..processing import process_result, spectrogram from .base import Stage -from .canvas import combo, group +from .controls import combo, group from .mirror_stack import MirrorStack from .retrieval_views import ViewPanel from .widgets import SciSpinBox, ValueControl diff --git a/croak/gui/stage_load.py b/croak/gui/stage_load.py index 8984e1f..b1513b7 100644 --- a/croak/gui/stage_load.py +++ b/croak/gui/stage_load.py @@ -27,7 +27,7 @@ from ..plotting.screen import raw_trace_map from ..session.pipeline import assemble_load_data from .base import Stage -from .canvas import check, combo, group +from .controls import check, combo, group from .retrieval_views import ViewPanel from .state import PreprocParams from .widgets import SciSpinBox diff --git a/croak/gui/stage_marginal_check.py b/croak/gui/stage_marginal_check.py index 61bf665..1959284 100644 --- a/croak/gui/stage_marginal_check.py +++ b/croak/gui/stage_marginal_check.py @@ -46,7 +46,7 @@ ) from ..session.pipeline import assemble_load_data, assemble_simulated_load_data from .base import Stage -from .canvas import add_wide_row, check, combo, dspin, group +from .controls import add_wide_row, check, combo, dspin, group from .retrieval_views import ViewPanel from .widgets import RangeControl diff --git a/croak/gui/stage_preprocess.py b/croak/gui/stage_preprocess.py index 572a80c..9a4d656 100644 --- a/croak/gui/stage_preprocess.py +++ b/croak/gui/stage_preprocess.py @@ -33,7 +33,7 @@ from ..preprocess import suggested_tau_exclude from ..session import pipeline from .base import Stage -from .canvas import ( +from .controls import ( add_wide_row, check, combo, diff --git a/croak/gui/stage_retrieve.py b/croak/gui/stage_retrieve.py index 4ef875e..f779c42 100644 --- a/croak/gui/stage_retrieve.py +++ b/croak/gui/stage_retrieve.py @@ -36,7 +36,7 @@ from ..truth_metrics import truth_errors from . import settings from .base import RunControls, Stage -from .canvas import check, collapsible, combo, dspin, group, spin +from .controls import check, collapsible, combo, dspin, group, spin from .readout import DASH, RetrievalReadout from .retrieval_views import RetrievalPanels from .widgets import SciSpinBox diff --git a/croak/gui/stage_simulated.py b/croak/gui/stage_simulated.py index 549ecb1..1155543 100644 --- a/croak/gui/stage_simulated.py +++ b/croak/gui/stage_simulated.py @@ -43,7 +43,7 @@ assemble_simulated_tracedata, ) from .base import Stage -from .canvas import add_wide_row, check, combo, group +from .controls import add_wide_row, check, combo, group from .retrieval_views import ViewPanel from .stage_load import seed_preproc_defaults from .widgets import RangeControl diff --git a/croak/gui/stage_synthetic.py b/croak/gui/stage_synthetic.py index dceba70..e2b3252 100644 --- a/croak/gui/stage_synthetic.py +++ b/croak/gui/stage_synthetic.py @@ -30,7 +30,7 @@ from ..processing import TruthPulse from ..pulses import gaussian_pulse, sech_pulse from .base import Stage -from .canvas import ( +from .controls import ( add_wide_row, check, combo, diff --git a/croak/gui/stage_uncertainty.py b/croak/gui/stage_uncertainty.py index a97cf5a..bc54f32 100644 --- a/croak/gui/stage_uncertainty.py +++ b/croak/gui/stage_uncertainty.py @@ -28,7 +28,7 @@ from ..processing import TruthPulse from ..uncertainty import UncertaintyResult, combine_uncertainties from .base import RunControls, Stage -from .canvas import check, combo, dspin, group, spin +from .controls import check, combo, dspin, group, spin from .retrieval_views import ViewPanel _METHODS = ["parametric", "delay", "frequency", "thickness", "covariance"] diff --git a/croak/plotting/__init__.py b/croak/plotting/__init__.py index 6a5fa27..2925413 100644 --- a/croak/plotting/__init__.py +++ b/croak/plotting/__init__.py @@ -9,7 +9,8 @@ (``draw_(ax, data)`` over one :class:`RetrievalPlotData` bundle, registered in :data:`RETRIEVAL_PANELS`). :func:`plot_retrieval` lays them out as the 3×4 overview; :func:`plot_retrieval_page` draws one of the three 2×2 pages -(:data:`RETRIEVAL_PAGES`: Traces, Pulse, Diagnostics) the GUI shows as tabs. +(:data:`RETRIEVAL_PAGES`: Traces, Pulse, Diagnostics), as the GUI's Analyse +pages show them. Figures are built with :external+matplotlib:class:`matplotlib.figure.Figure` directly (no ``pyplot`` global state), so they embed cleanly in a Qt canvas and also save @@ -18,15 +19,11 @@ Every public name is importable from ``croak.plotting`` itself; the submodules group them by domain: -``scale`` - On-screen text density (:class:`PlotScale`, :func:`plot_scale`, - :func:`scaled_rc`). ``units`` Display units and read-out formatting (:func:`sig3`, :func:`si_power`). ``traces`` FROG-trace images: colormaps, :func:`signed_pcolormesh`, the residual and - marginal plotters, :func:`plot_frog_filter`, :class:`FrogFilterView` and - :func:`plot_simulated_trace`. + marginal plotters, :func:`plot_frog_filter` and :func:`plot_simulated_trace`. ``pulse`` The retrieved pulse: :func:`plot_temporal`, :func:`plot_spectral` and :func:`plot_spectrogram`. @@ -34,15 +31,16 @@ The artist bundles each panel returns (:class:`TraceArtists`, …). ``panels`` :class:`RetrievalPlotData` and the twelve ``draw_`` panels. -``updates`` - The ``update_`` in-place twins of the panels, for the live preview. ``retrieval`` - The panel registry, pages and overview, :func:`plot_retrieval_page`, - :func:`plot_retrieval` and :class:`RetrievalPageView`. + The panel registry, pages and overview, :func:`plot_retrieval_page` and + :func:`plot_retrieval`. ``uncertainty`` Bootstrap and substrate-thickness uncertainty figures. ``figure`` The figure set-up the composites share. +``screen`` + What the GUI's interactive views show, as plain data + (:mod:`croak.plotting.screen`). """ from .artists import ( @@ -90,22 +88,10 @@ PageSpec, PanelDraw, PanelSpec, - PanelUpdate, - RetrievalPageView, plot_retrieval, plot_retrieval_page, ) -from .scale import ( - COMPACT_BELOW, - FONT_SCALE_MIN, - FONT_SCALE_STEP, - FULL_SCALE, - PlotScale, - plot_scale, - scaled_rc, -) from .traces import ( - FrogFilterView, cmap_negwhite, cmap_white, filter_view_vmax, @@ -127,30 +113,8 @@ si_power, sig3, ) -from .updates import ( - update_convergence, - update_convergence_curve, - update_delay_marginal, - update_freq_marginal, - update_measured_lin, - update_measured_log, - update_residual, - update_retrieved_lin, - update_retrieved_log, - update_spectral, - update_spectral_filter, - update_spectrogram, - update_temporal, -) __all__ = [ - "PlotScale", - "FULL_SCALE", - "FONT_SCALE_MIN", - "FONT_SCALE_STEP", - "COMPACT_BELOW", - "plot_scale", - "scaled_rc", "cmap_white", "cmap_negwhite", "signed_pcolormesh", @@ -195,23 +159,8 @@ "draw_freq_marginal", "draw_delay_marginal", "draw_spectrogram", - "update_measured_log", - "update_retrieved_log", - "update_measured_lin", - "update_retrieved_lin", - "update_temporal", - "update_spectral", - "update_convergence", - "update_convergence_curve", - "update_residual", - "update_spectral_filter", - "update_freq_marginal", - "update_delay_marginal", - "update_spectrogram", - "RetrievalPageView", "plot_frog_filter", "filter_view_vmax", - "FrogFilterView", "plot_simulated_trace", "plot_fwhm_distribution", "plot_intensity_band", diff --git a/croak/plotting/retrieval.py b/croak/plotting/retrieval.py index ffeec80..5b177c5 100644 --- a/croak/plotting/retrieval.py +++ b/croak/plotting/retrieval.py @@ -10,7 +10,7 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from matplotlib.figure import Figure from numpy.typing import ArrayLike @@ -36,21 +36,6 @@ retrieval_plot_data, ) from .pulse import SpectralAxis -from .updates import ( - _update_signature, - update_convergence, - update_delay_marginal, - update_freq_marginal, - update_measured_lin, - update_measured_log, - update_residual, - update_retrieved_lin, - update_retrieved_log, - update_spectral, - update_spectral_filter, - update_spectrogram, - update_temporal, -) if TYPE_CHECKING: from matplotlib.axes import Axes @@ -58,20 +43,15 @@ type PanelDraw = Callable[[Axes, RetrievalPlotData], PanelArtists] -# Any: each update_ takes its own artists dataclass, and a registry entry -# typed with the union would reject them (parameters are contravariant). The draw -# side keeps the precise union because a return type is covariant. -type PanelUpdate = Callable[[Any, RetrievalPlotData], None] @dataclass(frozen=True) class PanelSpec: - """One retrieval panel: registry key, axes title, draw and in-place update.""" + """One retrieval panel: registry key, axes title and how to draw it.""" key: str title: str draw: PanelDraw - update: PanelUpdate @dataclass(frozen=True) @@ -115,41 +95,18 @@ class ColorbarSpec: RETRIEVAL_PANELS: Mapping[str, PanelSpec] = { spec.key: spec for spec in ( - PanelSpec( - "measured_log", "Measured (log)", draw_measured_log, update_measured_log - ), - PanelSpec( - "retrieved_log", "Retrieved (log)", draw_retrieved_log, update_retrieved_log - ), - PanelSpec("temporal", "Retrieved pulse", draw_temporal, update_temporal), - PanelSpec( - "delay_marginal", - "Delay marginal", - draw_delay_marginal, - update_delay_marginal, - ), - PanelSpec( - "measured_lin", "Measured (lin)", draw_measured_lin, update_measured_lin - ), - PanelSpec( - "retrieved_lin", "Retrieved (lin)", draw_retrieved_lin, update_retrieved_lin - ), - PanelSpec("spectral", "Spectrum", draw_spectral, update_spectral), - PanelSpec( - "freq_marginal", - "Frequency marginal", - draw_freq_marginal, - update_freq_marginal, - ), - PanelSpec("convergence", "Convergence", draw_convergence, update_convergence), - PanelSpec("residual", "Residuals", draw_residual, update_residual), - PanelSpec( - "spectral_filter", - "Spectral filter", - draw_spectral_filter, - update_spectral_filter, - ), - PanelSpec("spectrogram", "Spectrogram", draw_spectrogram, update_spectrogram), + PanelSpec("measured_log", "Measured (log)", draw_measured_log), + PanelSpec("retrieved_log", "Retrieved (log)", draw_retrieved_log), + PanelSpec("temporal", "Retrieved pulse", draw_temporal), + PanelSpec("delay_marginal", "Delay marginal", draw_delay_marginal), + PanelSpec("measured_lin", "Measured (lin)", draw_measured_lin), + PanelSpec("retrieved_lin", "Retrieved (lin)", draw_retrieved_lin), + PanelSpec("spectral", "Spectrum", draw_spectral), + PanelSpec("freq_marginal", "Frequency marginal", draw_freq_marginal), + PanelSpec("convergence", "Convergence", draw_convergence), + PanelSpec("residual", "Residuals", draw_residual), + PanelSpec("spectral_filter", "Spectral filter", draw_spectral_filter), + PanelSpec("spectrogram", "Spectrogram", draw_spectrogram), ) } @@ -236,94 +193,6 @@ def _draw_panels( return axd, artists -class RetrievalPageView: - """Maintain a page of retrieval plots for repeated GUI updates. - - The live preview hands the Retrieve stage a new result a few times a second. - Clearing a figure and rebuilding a page's axes, colourbars and legends for - each one costs a full constrained-layout solve and lets legends jump between - frames; this view draws the page once and then pushes later results onto the - existing artists when :meth:`can_update` accepts the new data, as - :class:`FrogFilterView` does for the preprocessing figure. It redraws when - the grid, optional curves or panel structure change. - - Parameters - ---------- - page : PageSpec - Panels and their layout. - - Examples - -------- - >>> view = RetrievalPageView(RETRIEVAL_PAGES["pulse"]) # doctest: +SKIP - >>> view.draw(fig, data1) # doctest: +SKIP - >>> if view.can_update(data2): # doctest: +SKIP - ... view.update(data2) - ... else: - ... view.draw(fig, data2) - """ - - def __init__(self, page: PageSpec): - self.page = page - self.fig: Figure | None = None - self.axd: dict[str, Axes] = {} - self.artists: dict[str, PanelArtists] = {} - self._signature: tuple | None = None - - @property - def attached(self) -> bool: - """Whether the view has drawn into a figure.""" - return self.fig is not None - - def draw(self, fig: Figure, data: RetrievalPlotData) -> None: - """Clear ``fig`` and draw the page afresh for ``data``. - - Constrained layout is solved on the first real draw and then frozen (the - engine is swapped for matplotlib's placeholder), so later in-place updates - neither re-solve it nor nudge the panels as tick labels change width. - """ - _prepare_fig(fig, None) - self.fig = fig - self.axd, self.artists = _draw_panels(fig, self.page, data) - self._signature = _update_signature(data) - canvas = fig.canvas - - def freeze(_event) -> None: - canvas.mpl_disconnect(cid) - if self.fig is fig: - fig.set_layout_engine("none") - - cid = canvas.mpl_connect("draw_event", freeze) - - def can_update(self, data: RetrievalPlotData) -> bool: - """Whether ``data`` differs from the drawn result in values only.""" - return self.attached and _update_signature(data) == self._signature - - def update(self, data: RetrievalPlotData) -> None: - """Push ``data`` onto the drawn artists. - - Raises - ------ - RuntimeError - If nothing has been drawn yet. - ValueError - If :meth:`can_update` is false (different grid or optional artists); - :meth:`draw` it instead. - """ - if not self.attached: - raise RuntimeError("draw(fig, data) before update(data)") - if not self.can_update(data): - raise ValueError("the result's shape or contents changed: draw() it") - for key, artists in self.artists.items(): - RETRIEVAL_PANELS[key].update(artists, data) - - def reset(self) -> None: - """Forget the figure (someone else cleared it); the next call must draw.""" - self.fig = None - self.axd = {} - self.artists = {} - self._signature = None - - def plot_retrieval_page( data: RetrievalPlotData, page: PageSpec, diff --git a/croak/plotting/scale.py b/croak/plotting/scale.py deleted file mode 100644 index c4a4a71..0000000 --- a/croak/plotting/scale.py +++ /dev/null @@ -1,152 +0,0 @@ -"""On-screen text density of a figure squeezed below its design size.""" - -from __future__ import annotations - -from dataclasses import dataclass - -# --------------------------------------------------------------------------- -# On-screen density -# --------------------------------------------------------------------------- -# Matplotlib sizes text in points, so when a figure is squeezed into a small window -# the axes shrink and the text does not: on a 1366x768 laptop the twelve-panel -# retrieval summary spent 78 % of its area on titles, labels and padding. The GUI -# canvas (croak.gui.canvas.MplCanvas) therefore scales the rc font sizes by how far -# the canvas has fallen below the size its figure was designed for. The scale is a -# pure function of geometry, kept here so the plotting layer stays Qt-free and the -# thresholds are testable headlessly. - -#: Smallest fraction of the design font size that text may shrink to. Below ~70 % -#: a 10 pt label is 7 pt, the limit of comfortable legibility on a laptop panel. -FONT_SCALE_MIN = 0.7 -#: Quantisation step of :func:`plot_scale`. A window resize re-plots only when the -#: scale crosses a step, so a few pixels of drag never trigger a redraw. -FONT_SCALE_STEP = 0.05 -#: Below this scale :attr:`PlotScale.compact` is set, for plotting code that wants -#: to drop optional decoration (a second legend entry, a colorbar label) when tight. -COMPACT_BELOW = 0.9 - - -@dataclass(frozen=True) -class PlotScale: - """Text density of a figure relative to the size its layout was designed for. - - Parameters - ---------- - font : float - Multiplier applied to every rc font size (and text padding), in - ``[FONT_SCALE_MIN, 1.0]``. ``1.0`` is matplotlib's default 10 pt base. - - Examples - -------- - >>> PlotScale(0.85).compact - True - >>> FULL_SCALE.compact - False - """ - - font: float = 1.0 - - @property - def compact(self) -> bool: - """Whether the figure is tight enough to drop optional decoration.""" - return self.font < COMPACT_BELOW - - -#: The unscaled density: matplotlib's defaults. -FULL_SCALE = PlotScale() - - -def plot_scale( - width_in: float, height_in: float, design_in: tuple[float, float] -) -> PlotScale: - """Return the text density for a figure of the given size. - - The scale is the fraction of the design size that fits in the *tighter* - direction, ``min(width / design_width, height / design_height)``, quantised - to :data:`FONT_SCALE_STEP` and clamped to ``[FONT_SCALE_MIN, 1.0]``. A figure - at or above its design size is drawn at full scale; one squeezed to 60 % of - its design height gets the minimum. - - Parameters - ---------- - width_in, height_in : float - Current figure size in inches (``Figure.get_size_inches()``). On a Qt - canvas this is the logical pixel size / 100 on every display, because - matplotlib folds the device-pixel ratio into ``figure.dpi``. - design_in : tuple of float - ``(width, height)`` in inches the figure's layout was designed for — the - ``figsize`` a stage passes to its canvas. - - Returns - ------- - PlotScale - - Raises - ------ - ValueError - If any size is not positive. - - Examples - -------- - >>> plot_scale(9.91, 4.21, (12.0, 7.0)).font # Retrieve page at 1366x768 - 0.7 - >>> plot_scale(15.45, 7.33, (12.0, 7.0)).font # ... at 1920x1080 - 1.0 - """ - design_w, design_h = design_in - if min(width_in, height_in, design_w, design_h) <= 0: - raise ValueError( - f"figure and design sizes must be positive, got {width_in}x{height_in} " - f"in for a {design_w}x{design_h} in design" - ) - ratio = min(width_in / design_w, height_in / design_h) - # Quantise to the nearest step; round to two decimals so equal steps compare - # equal as floats (0.85, not 0.8500000000000001). - quantised = round(round(ratio / FONT_SCALE_STEP) * FONT_SCALE_STEP, 2) - return PlotScale(font=min(1.0, max(FONT_SCALE_MIN, quantised))) - - -# Matplotlib's default sizes spelled out in points. The rc defaults for the title -# and label keys are the *relative* strings 'large'/'medium', which cannot be -# multiplied; writing the resolved points also makes the scaling immune to a user -# matplotlibrc with absolute sizes. The four pads are included because text -# *padding* is a fixed-pixel cost too. -_SCALED_RC_BASE: dict[str, float] = { - "font.size": 10.0, - "axes.titlesize": 12.0, - "axes.labelsize": 10.0, - "xtick.labelsize": 10.0, - "ytick.labelsize": 10.0, - "legend.fontsize": 10.0, - "figure.titlesize": 12.0, - "axes.labelpad": 4.0, - "axes.titlepad": 6.0, - "xtick.major.pad": 3.5, - "ytick.major.pad": 3.5, -} - - -def scaled_rc(scale: PlotScale) -> dict[str, float]: - """Return rcParams overrides that shrink every font size and pad by ``scale``. - - Use with :external+matplotlib:func:`matplotlib.rc_context` *around the plotting - call*: matplotlib resolves relative sizes (``fontsize="small"``, the ``'large'`` - title default) against ``font.size`` when an artist is created, not when it is - drawn, so the context must enclose everything that creates text. - - Parameters - ---------- - scale : PlotScale - The density; :data:`FULL_SCALE` reproduces matplotlib's defaults. - - Returns - ------- - dict - ``{rc key: value in points}`` for the font-size and padding keys. - - Examples - -------- - >>> round(scaled_rc(PlotScale(0.7))["axes.titlesize"], 2) - 8.4 - """ - return {key: base * scale.font for key, base in _SCALED_RC_BASE.items()} diff --git a/croak/plotting/traces.py b/croak/plotting/traces.py index e3b7dfa..809d452 100644 --- a/croak/plotting/traces.py +++ b/croak/plotting/traces.py @@ -8,7 +8,6 @@ from __future__ import annotations -import contextlib from typing import TYPE_CHECKING import numpy as np @@ -351,178 +350,6 @@ def _decimate_for_display(C, x, y, *, cap=1000): return C[::sy, ::sx], x[::sx], y[::sy] -def _resample_uniform_x(x, C): - """Resample ``C`` (``(ny, nx)``) along axis 1 onto a uniform ``x`` grid. - - :external+matplotlib:class:`~matplotlib.image.AxesImage` (``imshow``) assumes evenly - spaced pixels and is positioned by a single linear ``extent``. A *wavelength* axis - derived from a uniform **frequency** grid (the simulated-trace case) is strongly - non-uniform, so a feature would otherwise be painted at the wrong wavelength (e.g. a - 260 nm signal drawn near 480 nm over a 150–800 nm span). Linearly interpolating each - delay row onto an evenly spaced ``x`` restores the correct positions; an - already-uniform axis (the frequency panels, or a uniform-λ spectrometer trace) is - returned unchanged. ``x`` must be ascending. - """ - x = np.asarray(x, dtype=float) - if x.size < 3: - return x, C - d = np.diff(x) - if np.allclose(d, d[0], rtol=1e-3, atol=0.0): - return x, C # already uniform — no interpolation needed - xu = np.linspace(x[0], x[-1], x.size) - Cu = np.empty_like(C) - for i in range(C.shape[0]): - Cu[i] = np.interp(xu, x, C[i]) - return xu, Cu - - -class FrogFilterView: - """Maintain a preprocessing figure for repeated GUI updates. - - The artist-reusing twin of :func:`plot_frog_filter`, which clears and - rebuilds the whole figure (6 panels × up to 2 ``pcolormesh`` + 4 - colourbars) on every call: ~150–400 ms, the dominant cost of the GUI - preprocessing redraw. Call :meth:`attach` once to create the panels, images - and colourbars, then :meth:`update` to push new data via - ``set_data``/``set_extent``/``set_clim`` onto the persistent images, which - benchmarks ~4× faster. Positive and negative samples use the same - signed-magnitude convention as :func:`signed_pcolormesh`. Non-uniform - wavelength arrays are interpolated onto a uniform display grid; the - underlying :class:`croak.preprocess.TraceData` arrays are unchanged. - """ - - _PANELS = ( - # key, title, xlabel-kind ("wl"/"hz"), db - ("a", "Measured (dB)", "wl", True), - ("b", "Filtered (dB)", "wl", True), - ("e", "Regridded (dB)", "hz", True), - ("c", "Measured (lin)", "wl", False), - ("d", "Filtered (lin)", "wl", False), - ("f", "Regridded (lin)", "hz", False), - ) - - def __init__( - self, *, tracedb: float = 30.0, cmap: str = "viridis", cap: int = 1000 - ): - self.tracedb = tracedb - self.cap = cap - self._cmap_pos = cmap_white(cmap) - self._cmap_pos.set_bad(alpha=0.0) # masked (negative) pixels stay clear - self._cmap_neg = _white_red() - self._cmap_neg.set_bad(alpha=0.0) - self.fig = None - self.axd = None - self.images: dict[str, tuple] = {} - - @property - def attached(self) -> bool: - """Whether a figure is currently attached to this view.""" - return self.fig is not None - - def attach(self, fig: Figure) -> None: - """Build the mosaic, images and colourbars once into ``fig``.""" - self.fig = fig - fig.clear() - with contextlib.suppress(Exception): - fig.set_layout_engine("constrained") - self.axd = fig.subplot_mosaic("abe\ncdf") - empty = np.zeros((1, 1)) - for key, title, kind, _db in self._PANELS: - ax = self.axd[key] - pos = ax.imshow( - empty, - origin="lower", - aspect="auto", - cmap=self._cmap_pos, - interpolation="nearest", - ) - neg = ax.imshow( - empty, - origin="lower", - aspect="auto", - cmap=self._cmap_neg, - interpolation="nearest", - ) - self.images[key] = (pos, neg) - ax.set_title(title) - ax.set_xlabel("Wavelength (nm)" if kind == "wl" else "Frequency (PHz)") - ax.set_ylabel("Delay (fs)") - # One positive + one negative colorbar per row (dB row a/b/e, lin row c/d/f). - self._cb_top = fig.colorbar( - self.images["a"][0], ax=[self.axd[k] for k in "abe"], shrink=0.5 - ) - self._cb_top_neg = fig.colorbar( - self.images["a"][1], - ax=[self.axd[k] for k in "abe"], - shrink=0.5, - label="neg |·|", - ) - self._cb_bot = fig.colorbar( - self.images["c"][0], ax=[self.axd[k] for k in "cdf"], shrink=0.5 - ) - self._cb_bot_neg = fig.colorbar( - self.images["c"][1], - ax=[self.axd[k] for k in "cdf"], - shrink=0.5, - label="neg |·|", - ) - - def _set_panel(self, key, x, y, C, *, db, vmax=None): - if self.axd is None: - raise RuntimeError("live plot is not attached to a figure") - pos_im, neg_im = self.images[key] - C = np.asarray(C, dtype=float) - C, x, y = _decimate_for_display(C, np.asarray(x), np.asarray(y), cap=self.cap) - # imshow paints evenly spaced pixels; a wavelength axis built from a - # uniform frequency grid (simulated traces) is non-uniform, so resample - # onto an even axis to place features at the right wavelength. - x, C = _resample_uniform_x(x, C) - neg = C < 0 - if db: - mag = np.clip( - 10 * np.log10(np.maximum(np.abs(C), 1e-30)), -self.tracedb, 0.0 - ) - vmin, vhi = -self.tracedb, 0.0 - else: - mag = np.abs(C) - vmin = 0.0 - vhi = float(np.nanmax(mag)) if vmax is None else vmax - extent = (float(x[0]), float(x[-1]), float(y[0]), float(y[-1])) - for im, masked in ( - (pos_im, np.ma.masked_where(neg, mag)), - (neg_im, np.ma.masked_where(~neg, mag)), - ): - im.set_data(masked) - im.set_clim(vmin, vhi) - im.set_extent(extent) - ax = self.axd[key] - ax.set_xlim(extent[0], extent[1]) - ax.set_ylim(extent[2], extent[3]) - - def update(self, td) -> None: - """Push a new :class:`~croak.preprocess.TraceData` onto the existing artists.""" - if not self.attached: - raise RuntimeError("attach(fig) before update(td)") - lam_nm = td.lam_frog / 1e-9 - freq = (td.omega + td.omega0_trace) / _TWOPI_PHZ - lin_vmax = filter_view_vmax(td) - self._set_panel("a", lam_nm, td.tau_meas / 1e-15, td.Ifrog_meas.T, db=True) - self._set_panel("b", lam_nm, td.tau_filt / 1e-15, td.Ifrog_filt.T, db=True) - self._set_panel("e", freq, td.delays / 1e-15, td.trace.T, db=True) - self._set_panel( - "c", lam_nm, td.tau_meas / 1e-15, td.Ifrog_meas.T, db=False, vmax=lin_vmax - ) - self._set_panel( - "d", lam_nm, td.tau_filt / 1e-15, td.Ifrog_filt.T, db=False, vmax=lin_vmax - ) - self._set_panel( - "f", freq, td.delays / 1e-15, td.trace.T, db=False, vmax=lin_vmax - ) - # The linear row's scale changes with the data; refresh its colorbars. - self._cb_bot.update_normal(self.images["c"][0]) - self._cb_bot_neg.update_normal(self.images["c"][1]) - - def plot_simulated_trace( omega, delays, diff --git a/croak/plotting/updates.py b/croak/plotting/updates.py deleted file mode 100644 index 1d704ce..0000000 --- a/croak/plotting/updates.py +++ /dev/null @@ -1,240 +0,0 @@ -"""In-place updates of the retrieval panels for the GUI's live preview.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np -from numpy.typing import ArrayLike - -from .artists import ( - ConvergenceArtists, - LineArtists, - MarginalArtists, - ResidualArtists, - SpectralArtists, - SpectrogramArtists, - TemporalArtists, - TraceArtists, -) -from .panels import RetrievalPlotData, _filter_factor, _trace_label -from .pulse import ( - _dispersion_text, - _edge_text, - _intensity_ylim, - _phase_ylim, - _spectral_curves, - _temporal_curves, - _truth_intensity, - _truth_spectral_phase, -) -from .traces import _signed_magnitude - -if TYPE_CHECKING: - from matplotlib.axes import Axes - - -# --------------------------------------------------------------------------- -# In-place updates -# --------------------------------------------------------------------------- -# Each update_(artists, data) pushes a new result onto the artists its -# draw_ twin created: set_data / set_array / set_text and explicit limits, -# never new artists. The invariant is update(draw(d1), d2) ≡ draw(d2), which the -# tests check panel by panel. Whether an update is possible at all — same grid -# shapes, same optional artists — is decided by _update_signature. - - -def _axes_of(artist) -> Axes: - """Return the axes an artist is drawn on (an updated artist always has one).""" - ax = artist.axes - if ax is None: - raise RuntimeError("the artist is not on any axes") - return ax - - -def _update_trace(artists: TraceArtists, data: RetrievalPlotData, C, *, db: bool): - mag, neg, vmin, vhi = _signed_magnitude( - C, db=db, tracedb=data.tracedb, vmax=None if db else data.lin_vmax - ) - artists.mesh_pos.set_array(np.ma.masked_where(neg, mag)) - artists.mesh_pos.set_clim(vmin, vhi) # a shared colorbar follows the norm - if artists.mesh_neg is not None: - artists.mesh_neg.set_array(np.ma.masked_where(~neg, mag)) - artists.mesh_neg.set_clim(vmin, vhi) - - -def update_measured_log(artists: TraceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_measured_log`'s artists in place.""" - _update_trace(artists, data, data.measured, db=True) - - -def update_retrieved_log(artists: TraceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_retrieved_log`'s artists in place.""" - _update_trace(artists, data, data.retrieved, db=True) - - -def update_measured_lin(artists: TraceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_measured_lin`'s artists in place.""" - _update_trace(artists, data, data.measured, db=False) - - -def update_retrieved_lin(artists: TraceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_retrieved_lin`'s artists in place.""" - _update_trace(artists, data, data.retrieved, db=False) - if artists.label is not None: - artists.label.set_text(_trace_label(data)) - - -def update_temporal(artists: TemporalArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_temporal`'s artists in place.""" - pr = data.processed - t = pr.t_over / 1e-15 - retr, tl, r_label, tl_label, ylabel, power_scale = _temporal_curves( - pr, data.power_scale - ) - artists.line_retr.set_data(t, retr) - artists.line_tl.set_data(pr.t_tl / 1e-15, tl) - artists.legend_retr.set_text(r_label) - artists.legend_tl.set_text(tl_label) - ax = _axes_of(artists.line_retr) - ax.set_ylabel(ylabel) - truth_curve = None - if artists.line_truth is not None and data.truth is not None: - truth_curve = _truth_intensity(data.truth, pr.energy, power_scale) - artists.line_truth.set_ydata(truth_curve) - ax.set_ylim(*_intensity_ylim(retr, tl, truth_curve)) - artists.line_phase.set_data(t[pr.mask_t], pr.phi_t[pr.mask_t]) - truth_phase = None # static within a run: the truth does not change - if artists.line_truth_phase is not None: - truth_phase = np.asarray(artists.line_truth_phase.get_ydata(), dtype=float) - limits = _phase_ylim(pr.phi_t[pr.mask_t], truth_phase) - if limits is not None: - artists.ax_phase.set_ylim(*limits) - - -def update_spectral(artists: SpectralArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_spectral`'s artists in place.""" - pr = data.processed - x, retrieved, measured, _xlabel = _spectral_curves(pr, data.spectral_axis) - artists.line_retr.set_data(x, retrieved) - if artists.line_meas is not None and measured is not None: - artists.line_meas.set_data(x, measured) - artists.line_phase.set_data(x[pr.mask_w], pr.phi_w[pr.mask_w]) - artists.line_phase_fit.set_data(x[pr.mask_w], pr.phi_w_fit[pr.mask_w]) - # The truth phase is static but its mask (where the spectrum is bright) moves. - tphi = _truth_spectral_phase(pr.wavelength, data.truth) - if artists.line_truth_phase is not None and tphi is not None: - artists.line_truth_phase.set_data(x[pr.mask_w], tphi[pr.mask_w]) - limits = _phase_ylim(pr.phi_w[pr.mask_w], None if tphi is None else tphi[pr.mask_w]) - if limits is not None: - artists.ax_phase.set_ylim(*limits) - artists.text_dispersion.set_text(_dispersion_text(pr)) - edge, edge_color = _edge_text(pr) - artists.text_edge.set_text(edge) - artists.text_edge.set_color(edge_color) - - -def update_convergence_curve(artists: ConvergenceArtists, errors: ArrayLike) -> None: - """Grow the error curve of :func:`plot_convergence` in place and rescale.""" - errors = np.asarray(errors, dtype=float) - artists.line.set_data(np.arange(1, errors.size + 1), errors) - ax = _axes_of(artists.line) - ax.relim() # autoscaling is still on here: plot_convergence sets no limits - ax.autoscale_view() - - -def update_convergence(artists: ConvergenceArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_convergence`'s artists in place.""" - update_convergence_curve(artists, data.processed.errors) - - -def update_residual(artists: ResidualArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_residual`'s artists in place.""" - resid = data.processed.residual - if artists.mesh is None or resid is None: - return - mr = float(np.max(np.abs(resid))) or 1.0 - artists.mesh.set_array(resid) - artists.mesh.set_clim(-mr, mr) - - -def update_spectral_filter(artists: LineArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_spectral_filter`'s artists in place (x is the fixed grid).""" - artists.line.set_ydata(_filter_factor(data.result)) - - -def update_freq_marginal(artists: MarginalArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_freq_marginal`'s artists in place.""" - pr = data.processed - artists.line_retr.set_ydata(pr.omega_marg_retr) - if artists.line_meas is not None and pr.omega_marg_meas is not None: - artists.line_meas.set_ydata(pr.omega_marg_meas) - - -def update_delay_marginal(artists: MarginalArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_delay_marginal`'s artists in place.""" - pr = data.processed - artists.line_retr.set_ydata(pr.tau_marg_retr) - if artists.line_meas is not None and pr.tau_marg_meas is not None: - artists.line_meas.set_ydata(pr.tau_marg_meas) - - -def update_spectrogram(artists: SpectrogramArtists, data: RetrievalPlotData) -> None: - """Update :func:`draw_spectrogram`'s artists in place. - - Raises - ------ - ValueError - If the spectrogram's shape differs from the drawn one (a different grid): - the panel has to be redrawn, not updated. - """ - _tc, _lam, S = data.spectrogram - array = artists.mesh.get_array() - drawn = () if array is None else tuple(np.shape(array)) - if drawn != S.shape: - raise ValueError(f"spectrogram shape changed from {drawn} to {S.shape}") - artists.mesh.set_array(S) - artists.mesh.autoscale() # what the first draw did implicitly - - -def _update_signature(data: RetrievalPlotData) -> tuple: - """Everything that decides which artists exist and the mesh geometry. - - Two bundles with equal signatures differ only in values, so a panel drawn for - one can be updated in place for the other. Components: the trace, grid and - oversampled-axis shapes; whether the traces hold negatives (the second - colormap); which optional curves exist (residual, measured spectrum and - marginals, absolute power, the truth and its phases); how many stage-handover - rules are visible; and the fixed axis bounds. - """ - pr = data.processed - result = data.result - truth = data.truth - n_rules = sum(1 for edge in result.stage_boundaries if 0 < edge < len(pr.errors)) - return ( - data.retrieved.shape, - data.measured.shape, - result.omega.size, - result.delays.size, - pr.t_over.size, - pr.t_tl.size, - pr.wavelength.size, - bool((data.measured < 0).any()), - bool((data.retrieved < 0).any()), - pr.residual is not None, - pr.Iw_meas is not None, - pr.omega_marg_meas is not None, - pr.tau_marg_meas is not None, - pr.peak_power is not None, - truth is not None, - truth is not None and truth.phi_t is not None, - truth is not None and truth.lam is not None and truth.Iw is not None, - truth is not None and truth.lam is not None and truth.phi_w is not None, - n_rules, - data.lam_min, - data.lam_max, - data.flim, - data.halfwidth, - data.tracedb, - data.spectral_axis, - ) diff --git a/tests/gui/test_base.py b/tests/gui/test_base.py index 370d854..6c39010 100644 --- a/tests/gui/test_base.py +++ b/tests/gui/test_base.py @@ -9,7 +9,7 @@ from PySide6.QtWidgets import QLabel, QLineEdit from croak.gui.base import Stage -from croak.gui.canvas import group +from croak.gui.controls import group from croak.gui.state import WizardState diff --git a/tests/gui/test_canvas.py b/tests/gui/test_canvas.py deleted file mode 100644 index b341e4a..0000000 --- a/tests/gui/test_canvas.py +++ /dev/null @@ -1,80 +0,0 @@ -"""MplCanvas re-plotting and the small widget factories.""" - -from __future__ import annotations - -import gc -import weakref - -import pytest - -pytest.importorskip("PySide6") - -from croak.gui.canvas import MplCanvas, check, combo, dspin, spin, with_toolbar - - -def test_canvas_keeps_its_window_alive_during_a_draw(qapp): - """A pending canvas draw must outlive its otherwise unreachable parent.""" - canvas = MplCanvas() - panel = with_toolbar(canvas) - # Model a discarded stage awaiting cyclic collection while its queued draw - # still owns a Python reference to the canvas, but not to the parent widget. - panel.cycle = panel - owner = weakref.ref(panel) - owners = [panel] - del panel - alive_during_draw = [] - - def collect_during_draw(_event): - # Release it only once the draw has started, regardless of when Python - # happens to collect cycles from earlier tests. - owners.clear() - gc.collect() - alive_during_draw.append(owner() is not None) - - canvas.mpl_connect("draw_event", collect_during_draw) - canvas.draw() - assert alive_during_draw == [True] - # Drawing should keep the window alive only for the duration of the draw. - gc.collect() - assert owner() is None - - -def test_canvas_draw_without_a_plot_function_is_an_error(qtbot): - canvas = MplCanvas() - qtbot.addWidget(canvas) - with pytest.raises(RuntimeError, match="render"): - canvas._draw_at(canvas.scale) - - -def test_canvas_replot_only_when_something_changed(qtbot): - canvas = MplCanvas() - qtbot.addWidget(canvas) - calls = [] - # Nothing rendered yet: a debounced resize has nothing to re-plot. - canvas._replot() - canvas.render(lambda fig: calls.append(fig)) - assert len(calls) == 1 - # Same density as drawn: no re-plot. - canvas._replot() - assert len(calls) == 1 - - -def test_widget_factories_without_a_change_handler(qapp): - assert spin(0, 10, 3).value() == 3 - assert dspin(0.0, 1.0, 0.25).value() == pytest.approx(0.25) - box = combo(["a", "b"], value="z") # unknown value: keeps the first item - assert box.currentText() == "a" - assert check("on", True).isChecked() - - -def test_widget_factories_connect_the_change_handler(qapp): - seen = [] - s = spin(0, 10, 3, on_change=seen.append) - s.setValue(4) - d = dspin(0.0, 1.0, 0.25, on_change=seen.append) - d.setValue(0.5) - c = combo(["a", "b"], value="a", on_change=seen.append) - c.setCurrentText("b") - k = check("on", False, on_change=seen.append) - k.setChecked(True) - assert seen == [4, 0.5, "b", True] diff --git a/tests/gui/test_controls.py b/tests/gui/test_controls.py new file mode 100644 index 0000000..130a29e --- /dev/null +++ b/tests/gui/test_controls.py @@ -0,0 +1,30 @@ +"""The small form-control factories.""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("PySide6") + +from croak.gui.controls import check, combo, dspin, spin + + +def test_widget_factories_without_a_change_handler(qapp): + assert spin(0, 10, 3).value() == 3 + assert dspin(0.0, 1.0, 0.25).value() == pytest.approx(0.25) + box = combo(["a", "b"], value="z") # unknown value: keeps the first item + assert box.currentText() == "a" + assert check("on", True).isChecked() + + +def test_widget_factories_connect_the_change_handler(qapp): + seen = [] + s = spin(0, 10, 3, on_change=seen.append) + s.setValue(4) + d = dspin(0.0, 1.0, 0.25, on_change=seen.append) + d.setValue(0.5) + c = combo(["a", "b"], value="a", on_change=seen.append) + c.setCurrentText("b") + k = check("on", False, on_change=seen.append) + k.setChecked(True) + assert seen == [4, 0.5, "b", True] diff --git a/tests/gui/test_wizard_flow.py b/tests/gui/test_wizard_flow.py index 332a1c0..66332b4 100644 --- a/tests/gui/test_wizard_flow.py +++ b/tests/gui/test_wizard_flow.py @@ -25,7 +25,7 @@ from PySide6.QtCore import QSize, Qt from croak.gui import settings -from croak.gui.canvas import MplCanvas, collapsible +from croak.gui.controls import collapsible from croak.gui.stage_load import assemble_load_data, seed_preproc_defaults from croak.gui.stage_marginal_check import StageMarginalCheck from croak.gui.state import WizardState @@ -2272,61 +2272,6 @@ def test_solver_switch_rescales_regularisation(qtbot): assert p.reg_spectrum == pytest.approx(0.05) -# -- responsive canvas --------------------------------------------------------- -def test_canvas_replots_when_a_resize_changes_the_density(qtbot): - canvas = MplCanvas(figsize=(12, 7)) - qtbot.addWidget(canvas) - calls = [] - canvas.render(lambda fig: calls.append(fig.add_subplot(111).set_title("t"))) - canvas.resize(1200, 700) # exactly the design size: no re-plot on show - canvas.show() - qtbot.waitExposed(canvas) - assert canvas.scale.font == 1.0 - assert len(calls) == 1 - canvas.resize(600, 350) # half size -> density clamps to the 0.7 minimum - qtbot.waitUntil(lambda: len(calls) == 2) - assert canvas.scale.font == pytest.approx(0.7) - assert canvas.figure.axes[0].title.get_fontsize() == pytest.approx(8.4) - - -def test_canvas_resize_without_a_density_change_does_not_replot(qtbot): - canvas = MplCanvas(figsize=(12, 7)) - qtbot.addWidget(canvas) - calls = [] - canvas.render(lambda fig: calls.append(fig.add_subplot(111))) - canvas.resize(1200, 700) - canvas.show() - qtbot.waitExposed(canvas) - canvas.resize(1180, 690) # a few pixels: same quantised density - qtbot.wait(3 * MplCanvas.REPLOT_DELAY_MS) - assert len(calls) == 1 - - -def test_canvas_save_figure_exports_at_the_design_size(qtbot, tmp_path): - """The export is drawn afresh at the design size, not the shrunken screen size.""" - canvas = MplCanvas(figsize=(12, 7)) - qtbot.addWidget(canvas) - seen = [] - - def plot(fig): - seen.append(tuple(float(v) for v in fig.get_size_inches())) - fig.add_subplot(111).set_title("t") - - canvas.render(plot) - canvas.resize(600, 350) - canvas.show() - qtbot.waitExposed(canvas) - qtbot.waitUntil(lambda: canvas.scale.font == pytest.approx(0.7)) - path = tmp_path / "fig.pdf" - canvas.save_figure(path, dpi=72) - assert path.stat().st_size > 0 - assert seen[-1] == pytest.approx((12.0, 7.0)) - blank = MplCanvas() - qtbot.addWidget(blank) - with pytest.raises(RuntimeError, match="nothing has been rendered"): - blank.save_figure(tmp_path / "none.pdf", dpi=72) - - # -- remembered layout ----------------------------------------------------------- @pytest.mark.parametrize( ("size", "available", "expected"), diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 0daef4c..a8662a0 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -93,47 +93,6 @@ def test_plot_frog_filter(experiment): assert len(fig.axes) >= 6 -def test_frog_filter_view_reuses_artists(experiment): - """FrogFilterView.update mutates the existing images, not rebuild them.""" - from matplotlib.figure import Figure - - exp = experiment - common = dict( - lam_min=exp.lam_min, - lam_max=exp.lam_max, - input_unit="delay", - filter_fringes=False, - dtau_min=None, - ) - td1 = preprocess.load_and_clean( - exp.trace, exp.lam_nm * 1e-9, exp.delay_fs * 1e-15, exp.interaction, **common - ) - # a different crop -> different delay count, exercising set_extent/shape change - td2 = preprocess.load_and_clean( - exp.trace, - exp.lam_nm * 1e-9, - exp.delay_fs * 1e-15, - exp.interaction, - tau_crop=(-30e-15, 30e-15), - **common, - ) - - fig = Figure() - view = plotting.FrogFilterView() - view.attach(fig) - view.update(td1) - images_before = {k: (id(p), id(n)) for k, (p, n) in view.images.items()} - n_axes = len(fig.axes) - - view.update(td2) # must not recreate axes/images/colorbars - images_after = {k: (id(p), id(n)) for k, (p, n) in view.images.items()} - assert images_before == images_after - assert len(fig.axes) == n_axes - # the regridded panel's data tracks the new (cropped) delay count - assert view.images["e"][0].get_array().shape[0] == td2.delays.size - assert td2.delays.size != td1.delays.size # the crop really changed it - - def test_single_axis_plotters(result_and_trace): from matplotlib.figure import Figure @@ -176,40 +135,6 @@ def test_si_power_prefixes(): assert plotting.si_power(0.0) == (1.0, "W") # unspecified falls back to W -def test_resample_uniform_x_leaves_uniform_axis_unchanged(): - x = np.linspace(200.0, 800.0, 64) - C = np.random.default_rng(0).random((10, 64)) - xu, Cu = plotting.traces._resample_uniform_x(x, C) - np.testing.assert_allclose(xu, x) - np.testing.assert_allclose(Cu, C) - - -def test_resample_uniform_x_places_feature_at_true_wavelength(): - """A λ axis from a uniform ω grid must not shift a feature's wavelength. - - Regression for the simulated-trace preprocess panel painting a 260 nm signal - near 480 nm: imshow's linear extent assumes even spacing, so the strongly - non-uniform (ω-uniform) λ axis must be resampled first. - """ - # uniform angular-frequency grid spanning 150–800 nm; descending ω gives an - # ascending (non-uniform) wavelength axis, as the simulated loader yields. - omega = np.linspace(wlfreq(150e-9), wlfreq(800e-9), 400) - lam_nm = wlfreq(omega) / 1e-9 # non-uniform, ascending (150 → 800 nm) - peak_nm = 260.0 - row = np.exp(-0.5 * ((lam_nm - peak_nm) / 5.0) ** 2) # narrow feature at 260 nm - C = np.tile(row, (8, 1)) # (ndelay, nlam) - - xu, Cu = plotting.traces._resample_uniform_x(lam_nm, C) - # on the uniform display axis the peak column maps linearly to its wavelength - j = int(np.argmax(Cu.sum(axis=0))) - wl = xu[0] + j / (xu.size - 1) * (xu[-1] - xu[0]) - assert wl == pytest.approx(peak_nm, abs=3.0) - # the raw (non-uniform) axis would have mis-placed it far to the red - i = int(np.argmax(C.sum(axis=0))) - wl_raw = lam_nm[0] + i / (lam_nm.size - 1) * (lam_nm[-1] - lam_nm[0]) - assert wl_raw > 400.0 # the bug being fixed - - def test_retrieval_annotations_carry_expected_precision(result_and_trace): """The error is annotated to 2 dp (%) and GDD/TOD to 1 dp. @@ -415,34 +340,6 @@ def test_filter_view_scale_ignores_out_of_band_negatives(experiment): assert plotting.filter_view_vmax(spoiled) == pytest.approx(1.0, rel=1e-9) -def test_filter_view_clims_follow_the_positive_scale(experiment): - """The linear panels take the positive scale; the dB row stays fixed.""" - from matplotlib.figure import Figure - - exp = experiment - td = preprocess.load_and_clean( - exp.trace, - exp.lam_nm * 1e-9, - exp.delay_fs * 1e-15, - exp.interaction, - lam_min=exp.lam_min, - lam_max=exp.lam_max, - input_unit="delay", - filter_fringes=False, - dtau_min=None, - ) - view = plotting.FrogFilterView(tracedb=30.0) - view.attach(Figure()) - view.update(td) - vmax = plotting.filter_view_vmax(td) - for key in ("c", "d", "f"): # the linear row - for image in view.images[key]: - assert image.get_clim() == pytest.approx((0.0, vmax)) - for key in ("a", "b", "e"): # the dB row is on its own fixed scale - for image in view.images[key]: - assert image.get_clim() == pytest.approx((-30.0, 0.0)) - - def test_filter_view_vmax_falls_back_when_nothing_is_positive(): """An all-negative trace still gets a usable, positive scale.""" from dataclasses import dataclass @@ -495,90 +392,6 @@ def test_plot_convergence_ignores_out_of_range_boundaries(): # -- on-screen density ------------------------------------------------------- -@pytest.mark.parametrize( - ("width", "height", "design"), - [(15.45, 7.33, (12.0, 7.0)), (13.53, 7.70, (12.0, 7.0)), (9.73, 5.73, (5.0, 4.0))], -) -def test_plot_scale_is_full_at_or_above_the_design_size(width, height, design): - """A canvas at or above its design size keeps matplotlib's default text size.""" - assert plotting.plot_scale(width, height, design) == plotting.FULL_SCALE - - -@pytest.mark.parametrize( - ("width", "height", "design", "expected"), - [ - (9.91, 4.21, (12.0, 7.0), 0.7), # Retrieve canvas at 1366x768 (clamped) - (9.91, 5.91, (9.0, 7.0), 0.85), # the 2x2 stages at 1366x768 - (9.73, 2.64, (5.0, 4.0), 0.7), # the stacked marginal-check pair - (9.91, 5.91, (11.0, 4.5), 0.9), # Uncertainty at 1366x768 - ], -) -def test_plot_scale_follows_the_tighter_dimension_and_clamps( - width, height, design, expected -): - assert plotting.plot_scale(width, height, design).font == pytest.approx(expected) - - -def test_plot_scale_is_quantised_to_the_step(): - """0.883 of the design height rounds to the nearest 0.05 step, 0.9.""" - font = plotting.plot_scale(10.6, 7.0, (12.0, 7.0)).font - assert font == pytest.approx(0.9) - steps = font / plotting.FONT_SCALE_STEP - assert steps == pytest.approx(round(steps)) - - -@pytest.mark.parametrize( - ("width", "height", "design"), - [(0.0, 4.0, (5.0, 4.0)), (5.0, 4.0, (5.0, 0.0)), (-1.0, 4.0, (5.0, 4.0))], -) -def test_plot_scale_rejects_non_positive_sizes(width, height, design): - with pytest.raises(ValueError, match="must be positive"): - plotting.plot_scale(width, height, design) - - -def test_plot_scale_compact_flag(): - assert plotting.PlotScale(0.85).compact - assert not plotting.PlotScale(0.9).compact - assert not plotting.FULL_SCALE.compact - - -def test_scaled_rc_keys_are_valid_and_scale_uniformly(): - """Every key is a real rc key, values scale together, full scale = defaults.""" - from matplotlib.font_manager import FontProperties - - small = plotting.scaled_rc(plotting.PlotScale(0.7)) - full = plotting.scaled_rc(plotting.FULL_SCALE) - with matplotlib.rc_context(small): # unknown keys would raise KeyError - pass - for key, value in small.items(): - assert value == pytest.approx(0.7 * full[key]) - for key, value in full.items(): - default = matplotlib.rcParamsDefault[key] - if isinstance(default, str): # 'large'/'medium' resolve against 10 pt - default = FontProperties(size=default).get_size_in_points() - assert value == pytest.approx(default) - - -def test_scaled_rc_sizes_artists_created_inside_the_context(): - """rc sizes bind at artist creation: inside the context text is scaled, and an - axes created afterwards is back at the defaults even on the same figure.""" - from matplotlib.figure import Figure - - with matplotlib.rc_context(plotting.scaled_rc(plotting.PlotScale(0.7))): - fig = Figure() - ax = fig.add_subplot() - ax.plot([0.0, 1.0], [0.0, 1.0], label="x") - ax.set_title("t") - ax.set_xlabel("x") - legend = ax.legend() - fig.canvas.draw() # drawing later, outside the context, does not undo it - assert ax.title.get_fontsize() == pytest.approx(8.4) - assert ax.xaxis.label.get_fontsize() == pytest.approx(7.0) - assert ax.xaxis.get_majorticklabels()[0].get_fontsize() == pytest.approx(7.0) - assert legend.get_texts()[0].get_fontsize() == pytest.approx(7.0) - late = fig.add_subplot() - late.set_title("late") - assert late.title.get_fontsize() == pytest.approx(12.0) # -- retrieval panels and pages ---------------------------------------------------- @@ -755,16 +568,6 @@ def _retrieve_on(n_delays, maxiters): return res, trace -@pytest.fixture(scope="module") -def update_results(): - """Two retrievals on one grid (same shapes, different values) and a third on - another delay grid (different shapes).""" - r1, trace = _retrieve_on(60, 60) - r2, _ = _retrieve_on(60, 15) - r3, trace3 = _retrieve_on(40, 15) - return (r1, trace), (r2, trace), (r3, trace3) - - def _artist_state(value): from matplotlib.axes import Axes from matplotlib.collections import QuadMesh @@ -817,28 +620,6 @@ def _assert_equal(x, y, where): assert x == y, (where, x, y) -@pytest.mark.parametrize("key", list(plotting.RETRIEVAL_PANELS)) -def test_update_panel_matches_a_fresh_draw(update_results, key): - """update(draw(d1), d2) shows exactly what draw(d2) shows, on the same artists.""" - from matplotlib.figure import Figure - - (r1, trace), (r2, _), _ = update_results - d1 = _plot_data(r1, trace) - d2 = _plot_data(r2, trace) - spec = plotting.RETRIEVAL_PANELS[key] - fig_u = Figure() - ax_u = fig_u.add_subplot() - artists = spec.draw(ax_u, d1) - ids = [id(a) for a in fig_u.axes] + [id(v) for v in vars(artists).values()] - spec.update(artists, d2) - assert [id(a) for a in fig_u.axes] + [id(v) for v in vars(artists).values()] == ids - fig_f = Figure() - ax_f = fig_f.add_subplot() - fresh = spec.draw(ax_f, d2) - for k, v in _panel_state(fresh, ax_f).items(): - _assert_equal(_panel_state(artists, ax_u)[k], v, f"{key}.{k}") - - def test_retrieval_legends_sit_in_fixed_corners(result_and_trace): """No retrieval-panel legend uses loc="best", which jumps between live frames.""" res, trace = result_and_trace @@ -849,115 +630,6 @@ def test_retrieval_legends_sit_in_fixed_corners(result_and_trace): assert legend._loc != 0, legend # 0 is matplotlib's code for "best" -def test_update_temporal_legend_follows_the_fwhm(update_results): - from matplotlib.figure import Figure - - (r1, trace), (r2, _), _ = update_results - artists = plotting.draw_temporal(Figure().add_subplot(), _plot_data(r1, trace)) - plotting.update_temporal(artists, _plot_data(r2, trace)) - pr2 = processing.process_result(r2, measured=trace) - assert plotting.units._fs(pr2.fwhm_retr) in artists.legend_retr.get_text() - assert plotting.units._fs(pr2.fwhm_tl) in artists.legend_tl.get_text() - - -def test_update_convergence_curve_grows_and_rescales(): - from matplotlib.figure import Figure - - ax = Figure().add_subplot() - artists = plotting.plot_convergence(ax, [1.0, 0.5, 0.3]) - line = artists.line - plotting.update_convergence_curve(artists, [1.0, 0.5, 0.3, 0.1, 0.01]) - assert artists.line is line - assert line.get_xydata().shape == (5, 2) - assert ax.get_xlim()[1] >= 5 - assert ax.get_ylim()[0] <= 0.01 - plotting.update_convergence_curve(artists, []) # a rehydrated result - assert line.get_xydata().shape == (0, 2) - - -def test_update_residual_colorbar_follows_the_new_scale(update_results): - from matplotlib.figure import Figure - - (r1, trace), (r2, _), _ = update_results - fig = Figure() - ax = fig.add_subplot() - artists = plotting.draw_residual(ax, _plot_data(r1, trace)) - assert artists.mesh is not None - cbar = fig.colorbar(artists.mesh, ax=ax) - d2 = _plot_data(r2, trace) - plotting.update_residual(artists, d2) - assert d2.processed.residual is not None - mr = float(np.max(np.abs(d2.processed.residual))) - assert artists.mesh.get_clim() == pytest.approx((-mr, mr)) - assert cbar.ax.get_ylim() == pytest.approx((-mr, mr)) - - -def test_update_signature_separates_shapes_from_values(update_results): - from croak.processing import TruthPulse - - (r1, trace), (r2, _), (r3, trace3) = update_results - d1 = _plot_data(r1, trace) - d2 = _plot_data(r2, trace) - d3 = _plot_data(r3, trace3) - assert plotting.updates._update_signature(d1) == plotting.updates._update_signature( - d2 - ) - assert plotting.updates._update_signature(d1) != plotting.updates._update_signature( - d3 - ) - g = Grid(96, dt=0.5e-15) - truth = TruthPulse.from_spectrum(g, gaussian_pulse(g, 7e-15), float(wlfreq(800e-9))) - assert plotting.updates._update_signature(d1) != plotting.updates._update_signature( - _plot_data(r1, trace, truth=truth) - ) - - -@pytest.mark.parametrize("key", ["traces", "pulse", "diagnostics"]) -def test_page_view_updates_in_place_and_falls_back_to_draw(update_results, key): - from matplotlib.figure import Figure - - (r1, trace), (r2, _), (r3, trace3) = update_results - view = plotting.RetrievalPageView(plotting.RETRIEVAL_PAGES[key]) - assert not view.attached - with pytest.raises(RuntimeError, match="draw"): - view.update(_plot_data(r1, trace)) - fig = Figure(layout="constrained") - view.draw(fig, _plot_data(r1, trace)) - axes_before = list(fig.axes) - d2 = _plot_data(r2, trace) - assert view.can_update(d2) - view.update(d2) - assert list(fig.axes) == axes_before - d3 = _plot_data(r3, trace3) - assert not view.can_update(d3) - with pytest.raises(ValueError, match="draw"): - view.update(d3) - view.draw(fig, d3) # the fallback: a fresh page - assert list(fig.axes) != axes_before - view.reset() - assert not view.attached - - -def test_page_view_freezes_the_layout_after_the_first_draw(update_results): - from matplotlib.backends.backend_agg import FigureCanvasAgg - from matplotlib.figure import Figure - from matplotlib.layout_engine import ( - ConstrainedLayoutEngine, - PlaceHolderLayoutEngine, - ) - - (r1, trace), _, _ = update_results - view = plotting.RetrievalPageView(plotting.RETRIEVAL_PAGES["pulse"]) - fig = Figure(layout="constrained") - FigureCanvasAgg(fig) - view.draw(fig, _plot_data(r1, trace)) - assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine) - fig.canvas.draw() # the first real draw solves the layout, then holds it - assert isinstance(fig.get_layout_engine(), PlaceHolderLayoutEngine) - view.draw(fig, _plot_data(r1, trace)) # a redraw solves it again - assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine) - - # -- spectrum axis -------------------------------------------------------------------- @pytest.mark.parametrize( ("axis", "xlabel"), @@ -1012,40 +684,6 @@ def test_frequency_spectrum_is_the_omega_density(result_and_trace): np.testing.assert_allclose(ax.lines[0].get_xdata(), omega_abs / (2 * np.pi * 1e15)) -def test_retrieval_plot_data_carries_the_spectral_axis(result_and_trace): - from matplotlib.figure import Figure - - res, trace = result_and_trace - default = _plot_data(res, trace) - assert default.spectral_axis == "frequency" - by_wavelength = _plot_data(res, trace, spectral_axis="wavelength") - ax = Figure().add_subplot() - plotting.draw_spectral(ax, by_wavelength) - assert ax.get_xlabel() == "Wavelength (nm)" - # a different abscissa is a different set of artists: no in-place update - assert plotting.updates._update_signature( - default - ) != plotting.updates._update_signature(by_wavelength) - fig = plotting.plot_retrieval(res, measured=trace, spectral_axis="wavelength") - spectrum = next(a for a in fig.axes if a.get_title() == "Spectrum") - assert spectrum.get_xlabel() == "Wavelength (nm)" - - -def test_update_spectral_matches_a_fresh_draw_on_the_wavelength_axis(update_results): - from matplotlib.figure import Figure - - (r1, trace), (r2, _), _ = update_results - d1 = _plot_data(r1, trace, spectral_axis="wavelength") - d2 = _plot_data(r2, trace, spectral_axis="wavelength") - ax_u = Figure().add_subplot() - artists = plotting.draw_spectral(ax_u, d1) - plotting.update_spectral(artists, d2) - ax_f = Figure().add_subplot() - fresh = plotting.draw_spectral(ax_f, d2) - for k, v in _panel_state(fresh, ax_f).items(): - _assert_equal(_panel_state(artists, ax_u)[k], v, f"spectral.{k}") - - # --------------------------------------------------------------------------- # Uncertainty figures # --------------------------------------------------------------------------- @@ -1171,36 +809,6 @@ def _variant_kwargs(res, variant): return {"processed": processing.process_result(res)} -@pytest.mark.parametrize("variant", ["truth", "truth_without_phases", "bare"]) -@pytest.mark.parametrize("key", list(plotting.RETRIEVAL_PANELS)) -def test_update_panel_matches_a_fresh_draw_with_optional_curves( - update_results, key, variant -): - """The optional curves (truth, measured spectrum, absolute power, missing - residual) update exactly as a fresh draw shows them.""" - from dataclasses import replace - - from matplotlib.figure import Figure - - (r1, trace), (r2, _), _ = update_results - # a per-frequency R_omega factor, so the spectral-filter panel has a shape - r1 = replace(r1, mu=np.linspace(1.0, 2.0, r1.omega.size)) - r2 = replace(r2, mu=np.linspace(2.0, 1.0, r2.omega.size)) - kw1, kw2 = _variant_kwargs(r1, variant), _variant_kwargs(r2, variant) - d1 = plotting.retrieval_plot_data(r1, measured=trace, **kw1) - d2 = plotting.retrieval_plot_data(r2, measured=trace, **kw2) - spec = plotting.RETRIEVAL_PANELS[key] - fig_u = Figure() - ax_u = fig_u.add_subplot() - artists = spec.draw(ax_u, d1) - spec.update(artists, d2) - fig_f = Figure() - ax_f = fig_f.add_subplot() - fresh = spec.draw(ax_f, d2) - for k, v in _panel_state(fresh, ax_f).items(): - _assert_close(_panel_state(artists, ax_u)[k], v, f"{key}.{k}") - - def _assert_close(x, y, where): """Like _assert_equal, to 1e-6 relative (1e-9 for scalars). @@ -1272,78 +880,6 @@ def test_plot_spectral_frequency_axis_with_a_measured_spectrum(result_and_trace) plt.close(fig) -def test_flat_phases_leave_the_phase_axis_unscaled(result_and_trace): - """With no finite phase sample there is no phase range to set.""" - from dataclasses import replace - - import matplotlib.pyplot as plt - - res, trace = result_and_trace - pr = processing.process_result(res, measured=trace) - pr = replace( - pr, phi_t=np.full_like(pr.phi_t, np.nan), phi_w=np.full_like(pr.phi_w, np.nan) - ) - fig, (a, b) = plt.subplots(1, 2) - data = plotting.retrieval_plot_data(res, measured=trace, processed=pr) - t_art = plotting.draw_temporal(a, data) - s_art = plotting.draw_spectral(b, data) - # matplotlib's own autoscaled limits, which no phase range overrides - t_lim, s_lim = t_art.ax_phase.get_ylim(), s_art.ax_phase.get_ylim() - plotting.update_temporal(t_art, data) - plotting.update_spectral(s_art, data) - assert t_art.ax_phase.get_ylim() == t_lim - assert s_art.ax_phase.get_ylim() == s_lim - plt.close(fig) - - -def test_axes_of_an_unplaced_artist_raises(): - from matplotlib.lines import Line2D - - with pytest.raises(RuntimeError, match="not on any axes"): - plotting.updates._axes_of(Line2D([0], [0])) - - -def test_trace_update_with_negative_samples_and_no_label(result_and_trace): - """A trace holding negatives keeps its second (negative) image in step, and - an unlabelled trace panel updates without a label.""" - from matplotlib.figure import Figure - - res, trace = result_and_trace - signed = trace - 0.05 * trace.max() # background-subtracted: negatives - data = plotting.retrieval_plot_data(res, measured=signed) - ax = Figure().add_subplot() - artists = plotting.draw_measured_lin(ax, data) - assert artists.mesh_neg is not None and artists.label is None - plotting.update_measured_lin(artists, data) - plotting.update_retrieved_lin(artists, data) # no label to refresh - assert artists.mesh_neg.get_clim() == artists.mesh_pos.get_clim() - - -def test_update_residual_without_a_residual_is_a_no_op(result_and_trace): - from matplotlib.figure import Figure - - res, trace = result_and_trace - bare = plotting.retrieval_plot_data(res, processed=processing.process_result(res)) - ax = Figure().add_subplot() - artists = plotting.draw_residual(ax, bare) - assert artists.mesh is None - plotting.update_residual(artists, bare) # nothing drawn, nothing to update - - -def test_update_spectrogram_refuses_a_new_shape(update_results): - from matplotlib.figure import Figure - - (r1, trace), _, _ = update_results - ax = Figure().add_subplot() - artists = plotting.draw_spectrogram(ax, _plot_data(r1, trace)) - # a narrower wavelength band keeps fewer rows of the spectrogram - narrower = plotting.retrieval_plot_data( - r1, measured=trace, lam_min=760e-9, lam_max=900e-9 - ) - with pytest.raises(ValueError, match="spectrogram shape changed"): - plotting.update_spectrogram(artists, narrower) - - def test_mappables_of_a_curve_panel_are_empty(): from matplotlib.lines import Line2D @@ -1375,21 +911,6 @@ def test_page_without_a_residual_or_with_negatives_lays_out_its_colorbars( assert len(fig.axes) == 12 + 2 + 2 * 2 + 1 -def test_page_view_redrawn_into_another_figure_leaves_the_old_one(update_results): - from matplotlib.backends.backend_agg import FigureCanvasAgg - from matplotlib.figure import Figure - from matplotlib.layout_engine import ConstrainedLayoutEngine - - (r1, trace), _, _ = update_results - view = plotting.RetrievalPageView(plotting.RETRIEVAL_PAGES["pulse"]) - first = Figure(layout="constrained") - FigureCanvasAgg(first) - view.draw(first, _plot_data(r1, trace)) - view.draw(Figure(layout="constrained"), _plot_data(r1, trace)) - first.canvas.draw() # the stale figure's freeze must not touch it - assert isinstance(first.get_layout_engine(), ConstrainedLayoutEngine) - - def test_filter_view_vmax_ignores_a_window_outside_the_axis(): from dataclasses import dataclass @@ -1417,21 +938,6 @@ def test_decimate_for_display_strides_large_images(): assert out.shape == (1250, 3) and y.size == 1250 and x.size == 3 -def test_resample_uniform_x_passes_short_axes_through(): - x = np.array([1.0, 3.0]) - C = np.ones((2, 2)) - xu, Cu = plotting.traces._resample_uniform_x(x, C) - assert xu is not None and Cu is C - - -def test_frog_filter_view_requires_attach(): - view = plotting.FrogFilterView() - with pytest.raises(RuntimeError, match="attach"): - view.update(None) - with pytest.raises(RuntimeError, match="not attached"): - view._set_panel("a", np.arange(2.0), np.arange(2.0), np.ones((2, 2)), db=False) - - def test_plot_simulated_trace_with_negative_samples(result_and_trace): res, trace = result_and_trace fig = plotting.plot_simulated_trace( @@ -1464,3 +970,42 @@ def test_plot_frog_filter_without_negative_samples(experiment): fig = plotting.plot_frog_filter(clipped) # six panels and one colorbar per row assert len(fig.axes) == 6 + 2 + + +def test_retrieval_plot_data_carries_the_spectral_axis(result_and_trace): + from matplotlib.figure import Figure + + res, trace = result_and_trace + default = _plot_data(res, trace) + assert default.spectral_axis == "frequency" + by_wavelength = _plot_data(res, trace, spectral_axis="wavelength") + ax = Figure().add_subplot() + plotting.draw_spectral(ax, by_wavelength) + assert ax.get_xlabel() == "Wavelength (nm)" + fig = plotting.plot_retrieval(res, measured=trace, spectral_axis="wavelength") + spectrum = next(a for a in fig.axes if a.get_title() == "Spectrum") + assert spectrum.get_xlabel() == "Wavelength (nm)" + + +def test_flat_phases_leave_the_phase_axis_unscaled(result_and_trace): + """With no finite phase sample there is no phase range to set.""" + from dataclasses import replace + + from matplotlib.figure import Figure + + from croak.plotting import screen + + res, trace = result_and_trace + pr = processing.process_result(res, measured=trace) + pr = replace( + pr, phi_t=np.full_like(pr.phi_t, np.nan), phi_w=np.full_like(pr.phi_w, np.nan) + ) + data = plotting.retrieval_plot_data(res, measured=trace, processed=pr) + fig = Figure() + a, b = fig.subplots(1, 2) + t_art = plotting.draw_temporal(a, data) + s_art = plotting.draw_spectral(b, data) + # matplotlib's own autoscale, which no phase range overrode + assert t_art.ax_phase.get_autoscaley_on() and s_art.ax_phase.get_autoscaley_on() + assert screen.temporal_view(data).right_range is None + assert screen.spectral_view(data).right_range is None diff --git a/tests/test_plotting_screen.py b/tests/test_plotting_screen.py index 64a718c..96245af 100644 --- a/tests/test_plotting_screen.py +++ b/tests/test_plotting_screen.py @@ -327,3 +327,21 @@ def test_screen_uncertainty_band_of_nothing_has_no_window(): flat = replace(_estimate(), profiles=np.zeros((40, 30))) _, band = screen.uncertainty_views(flat) assert band.xrange is None + + +def test_screen_filter_is_the_peak_normalised_factor_else_flat(data): + flat = replace(data, result=replace(data.result, mu=1.0)) + assert_allclose(screen.filter_view(flat).curves[0].y, 1.0) + n = data.result.omega.size + shaped = replace(data, result=replace(data.result, mu=np.linspace(1.0, 4.0, n))) + factor = screen.filter_view(shaped).curves[0].y + assert factor.max() == pytest.approx(1.0) and factor.min() == pytest.approx(0.25) + + +def test_screen_truth_spectrum_on_either_axis_or_none(data): + by_wavelength = replace(data, spectral_axis="wavelength") + roles = [c.role for c in screen.spectral_view(by_wavelength).curves] + assert "truth" in roles + no_spectrum = replace(data, truth=replace(data.truth, lam=None)) + roles = [c.role for c in screen.spectral_view(no_spectrum).curves] + assert "truth" not in roles From e974727471c70ed616a3a1a68900f8af72c24968 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 23:01:26 +0000 Subject: [PATCH 14/41] Screenshot the workflow window for the docs; fix a stray Home hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/make_docs_images.py now drives MainWindow through a synthetic retrieval and grabs Home, Retrieve and Analyse's Pulse page at 1440×900. Rebuilding Home's recent list hides the old entries at once, which deleteLater alone left drawn loose on the page; tile text sits at the top. Co-Authored-By: Claude Opus 5.5 --- croak/gui/home.py | 4 ++ docs/_static/gui_analyse.png | Bin 0 -> 133161 bytes docs/_static/gui_home.png | Bin 0 -> 48852 bytes docs/_static/gui_retrieve.png | Bin 310653 -> 196262 bytes tests/gui/test_window.py | 2 + tools/make_docs_images.py | 107 ++++++++++++++++++---------------- 6 files changed, 63 insertions(+), 50 deletions(-) create mode 100644 docs/_static/gui_analyse.png create mode 100644 docs/_static/gui_home.png diff --git a/croak/gui/home.py b/croak/gui/home.py index db2599d..276288e 100644 --- a/croak/gui/home.py +++ b/croak/gui/home.py @@ -76,6 +76,7 @@ def _tile(title: str, text: str) -> _Clickable: layout.setSpacing(4) layout.addWidget(label(title, "tileTitle")) layout.addWidget(label(text, "tileText", wrap=True)) + layout.addStretch(1) # tiles in a row share a height; text stays at the top return tile @@ -157,6 +158,9 @@ def show_recent(self, paths: Sequence[str]) -> None: """List the sessions at ``paths`` that still exist, newest first.""" for old in self._recent_widgets: self.recent.removeWidget(old) + # Hidden at once: deleteLater alone leaves it drawn, loose in the + # page, until the event loop next runs. + old.hide() old.deleteLater() self._recent_widgets: list[QWidget] = [] self.recent_items = [] diff --git a/docs/_static/gui_analyse.png b/docs/_static/gui_analyse.png new file mode 100644 index 0000000000000000000000000000000000000000..752eb827bcb4f102dec3a87f1ea3f82718096eed GIT binary patch literal 133161 zcmce7byQW|yDkO_qJUD;ZcF#dDO-sk7yCkI>q*IXY?rt{Co&Nm3bIu*( z{(0|RV>8&XVy-#g^~Uo&Z=kHS$ctxq&kzt0UWkbb$|E2krXe6af+9Tx-%K(*@CE;n zZA8`V5fD(@|NK3Op+Ut*KzM^7CdjAglC(SLsH!-H|MZZl?e`0kXFi|PqLD;JgI_u6 zSL=PMcj{6%uMe7|tJ3okf9vxZDd2O^@Z9r93UGK=4K1(uM@-%3RO1v+qXF)tl%4JV z2K)WmiklmcKiB@V{f-<;<0pT;T@8hTce5h~{wfX1m$6)yFWB-zpgRkP+O&(%M zt`~<(ztaV3HLXlcHlwK&63C+S^77Ks(t61;g}6N~-m$P$>eW!-=29JkD3s>~ma7!{vY zQg(T5ZLHDVnefk+OhJtL&*ADJ!uu^phf8hqjqWN`{oUPhA8hryLJ5nCieyr_rhfAn z8W>!`4-VGW$X$BY><(sYnZWH*w~c)WDlb3zmB@w+F*G#H$;pAyYrQ)Cq^tY#_3OzR z_VLZ5l`eXE`uzNS0l(*g0Ree-rvu5H#l^*(&ikXo!^0C33qN$*O4JhEE-o(RB6Y|6yIVhDFo@SWItyUU`MZO0 z7=nU>5ewL<{R%8G!R_hj=@)sXS>pwDH2BX!6q#|^U>ca z`zSC*(^BfNC4c5k!0Xk}$x&KbIy5w-QEfFgI=VYkZ4Cx^FyA=y+k}9SaBXc3@kL`p zgQk`ic)P!&YU`swI}}21Q0r zetC681jaE}?_8qY`0V*}2Rl16ljqQIu*fR&_6FVI5UflQVPU6(d96cvRn?1~i4vu0 zYVasfQ$GVw;r4he+CO9YOrcov*uSEpA~I5b&XJjg#gtiHQBlznH{1;mtlUzYzf3X* zIR(Y~?ylyb-0JFTo%4a$`R-Ik28n`#f{MxnnDR!iTep+-U%am*&xZDPcZ+qJwMCMX zl9D1LU$saXH+fuUq^A!-AN*s+A4BkJdrTbt{QRh?sZns~CB?;oO@I+DbcW#L4~2m# zZfd&AkxKMKBc`XN)x4KWN=`;2=9#T`w&qCQUtYG`pJfLdH+Lg(vMM>STrV##B*Y`_}J+h=Mn968B!ex33S}ot9?}hp?@konEObxE2)M-rinLZiqsX z9!a>my!?8nq}ge2I=iT7Z~xC`rK>Af`w$|iq@*M-Uj{6VR3sFvj(V+~p+d($Gk{Q; zt~5)M^*uT|8VMOWFgTdavicXJxTT$)-Rjz!FBF;|t<&IQ3m)x=hzLmv1#mx-l7(um z|5)`!$@d8&f)^x{UdzVUESaNVs#qOWOxlYy{yBfzP*c=mo*hj23x&?9T$d`ZbiXWQA|zfWbo{AVeC!dY&~ss9~Rvuf0w5aPe5sD0D&?}E`0k%GCd)&FIn+2*bi$(I7}3!F_7kjb1f>U+dm!doiZ&* zO4`gLp_!Wfazk4q_-Ec0{V`)>tDAP8K%l?Yu*xQX5h5Z~czFaRxb5SX;WvN$nOkAH zky3q;?N|ACmeP(?E$9=wtoPq~VY9-z^z0KkpCOp64Lt66;w28gy`y=^lbwtlWcg=@ zzmtg<)?vD~aIbOu_(9rKvNz$xEaaxTVi_a!KXLQpitS~Q)1}|Xg48z%%=K~KZq`wk znzOM|ZT>mB`q1B9tNj^D`35$$e z4ufOgQ$a4IT;$|zRRp*>%_ZoYv7Q+3bln2h zKs}1;M7ey5fIr~f*Yy01*@cdl>DijltAh-E429vRXSO#=4_H5TXd*%OVqyp^`mfKc z^xr`f*z$EsdR>21v+f*NFpw$kxE^{f=jLZvSmbGBbFr2Dybjb+Q>!fw9hZ3yhuNsw zld)HS>iP6Ej7K@dgeWz|{@iu*wZ=+SnYJdZZO*ndnYe)_yg65;NE~rhzs?Lld9)dR+PPi_GUrU)!_C{&pH2 zggZFZa^d$qEq4yq%~=#1*rI`fvz{kSwtrUl!Zk;AYoE|mLN2T3ThSXT?38Hk)$TSf zBQ^Uj#Is=oWYnOH?MuhT(1f#ttn>?yyJF9j@*ElUS_2*k86$Ph{oxUV4kG8I8e3i} zltIi_s3Rme-A==meQF0s=EjHb0fKsxQ?I2ap(PTe?J38g*4;*o2AcbnS}d$wMyKhF z=e#`NU0Y4T{Na~@$XClE1Dc*#H?int26F>j7=Gq?-<3;7`t40;I%{*WAX%_vLs8Y$ z#Ofj8tKrFvumM(384p&*WW)3XW&g6LY#JDUap%xS&v>gv8g% zVdc?EReB>SKCL9pFzXCn56$M$%;V8n%5Rg-s}1H18hk2Ysm&h8yM}ynwjwMsJ(>P_ z&L++Go323_*aHcoNL1AHQw=-aX-%Q|dokYC<+ku&PjPLBYHuy7=3TI133Eb9f#|KY z#%R+{IjP^RHaDWfw%EV^dU5yKf)YYvcQuu0UP8k3EXcCXL~_s4DnDDjwgA`P8VLoN zFyTEEUA~{1xanlJJ!LNWn}c$aH~Kldk}`i`u5PXpDFL~lV8*KvHrLC}9D~Z3sDyok z_nlS6zH$f%_iD^|YP>&5D7c#s7U0j+c5Shf!$No1dnK=b=s4K>KRG+jbhnYktXw#8 zxnEjgIiqu{L!GPI`TVK*TiT**7f+kA^<CuZ(<2dDZIIU>x3%81G9Jjm+R#O*8 zQmbtn{1g16hO0!cuZbA!Y3#!xiO=rIT6j!)`I~g@{Z7F79o* zn+AQD@z!rPMruFw$oQB&9^=3-@n3yC7kHg_i^`TnWBhz3o%T*D95y-U^h-a{<~|?= zw@;wyH{Y;AXQHH$I`-%h!iD9~mVq8I8MiSgUJhg(D4l-hzT%#}_xDX4D=*;)DH7dwYrDaT!@}2WbuWCcS;S zRtA%}#Fd0DSAFs2h1a$c8MQ<4LXl`U<=#Cf3| zsrS-jTf%5Q`*U^U^GdqPF&SR>6RvH0ajT`2`Ug&K3Es~u&}2mC>~7U!=AVzo5DT?u zZ{WP-PZ4bQ79V6e$g0{%@jmQYRU&l@aejB^o*EvlrK+gTdUQ~rNpxQ#cFR$1U4FHk z`4dgY`=}kb3~1HxE6c3*hrHQ1Ws}hGV8~H9!=+H}I&vt-&UZvw+}Lt+_0hT^8ciuv z=Cal?X1hHTS7bRpUwo!(O?zi7t^@P5JBOCE!eoWie&W@vnRK4#S%;lej2#<`6 zMZjP)HIDG35gg3bwIuf`5y2)nmlws7`Eu#k`b~!`LAa~j0k3LYe4I0z*FH{-(+Db} z>t)L$O&YY**@LwtA)4%LGH&>uY&gu^B#Du6_?(qHws;;B_qL^_%HP#l|75=t8lF-$ zuHm$M9et>zAwkMX2_G6Uo}ee#P`DYb_%;J4-k-06&~e*eCj6SK!+TCV?0wV_kBBHl zPEMXUPN#BkxJMbyIUnX^sg(-T`C&H~dz;cVQLIWph{0(;KYU3v!d9YTdpBGuwttes z-5^&xL&m;mjHZo+;xz4Sq7=4Ha59%&pHn9Kj-4qu*xK$Ra zQ(_94H8loX`BM7AsC&)!C&|}gIJ82#=z_3$$<%3{SKy?mfb5NPLcMB9`GSQ%;A>3$%qJCoQC&GPMM*A* zpqQN3{dkOg_SF)Q!^jf!#lq8pl5b%PdG+Qn2o4*ZYt~FAEg%chcxbUp`r%wZdeeX zQe;H=soR-d9XPRScTuikzJKa4p%NLxJfU2VgIDelAkZPlW=`sCjDqdIf^qhKu?==N zclNNVKQ;=J5^8|+{@&}5u|E7e8I78pcUt`2-Sro^fc{)E-M(+9gPhWKM!sO~&})af zpa#kNeK#ge+AZekUlT*V{bkwvsD5U>SA@}?B)oI3C-cfOD-$xr2#ahmHAOs_+Pjf~ zkK&3KJ^rgdWlT4|t~~a~BUTSOZz;uFey7E$jTEY) z5^gu9qT|f~Mb{fI-ShoEz*9<9PT%<2eE#y-0hZ!!)hyFzKHhVy=`W-6!VRO;b&cy+ zC?OKWnTV&}>3XPNjC8N)7ZSvlxY=Uf)8KP(+otZVm)UFH=*73Rv>+p@tErhxHaBMB z$X~<3tP64W#<^^2NTSPoLrrqLI4(r@E3|`L^6UP&o?b?Eams$y_XwEI;^@NO(8xyV zQEEjcbB+`q4K+1mS_xB#FsJSMvL9XjL!HLrq(!7>$XX40jo&Ip$LzaK9q=z}S8F!* z^>gWG8}GYMCQVIEzn7Bw7pvFF%BXb7>cHo}QUoPeG&B37O;<6543_G42T$#oy+;!c17vf84r^Qe&?qqF8E ze=J>!`^Mc0o9;{cMndZ9B31d9IQLZacM3li1e9W&py*OB94X2x>?W0-7}|e;hL82( zaIkw_kKVC%`=iS%Dss8Ce;U-!U0LSs=@y-sG`9T0vJbGYn}@jZ>`-(e*Qx%(S#xKi z4)41Jn2spY%PfTaJeg!2Ndc{~p>ZGdVQn?BW(i8X5Q>Y~dF|SeG=6C-9*Dutygrr` zwbyNU^o^5eNUX5`59foEV-%1S?Q?mY^PDTli*pFcLD7j|B2eF*DP5-FaYbjR3SipL zy>Xa4Z;-NCpM7ar%JQ;RmMkhun!TBA@QDv*Q;c3+$9j#wZRWU({)))ZQkUSK-L_%o z^TMIUtm{b%dfC>r0$y^gLOZQNEJ**AHC;$3`rkkUP*D0Lf?z);F@;=tp1EONv#_!x z%7DwG-ruvtm!X&PjxMKqw|_~_yY-JLdILtN1h>}W>9oj zcD2;&32K#1E4{4jsR;Drb1lr&LAKvJ!(s|w(KFsK7@J$Vo`gD2&WL)>4TPeyRv&O{ zHt2auTWO1n<3R+&_40mv+EPdLFE^j@6E;McZfG2OH=&?rLQFR!L%%8!&f zSW-0j1n~*N@7)gsR+IJcV>?E>GDd%CZIFnlHhpr_An%~74;xjzziOmhC^`wQG@t40 zNiWtSLzGZdrb|d)$i{#`m6Yir^c12#inFd{+@u=n;_}+k6kLq!EeiLz7<2uS&pmH^ zis!r(2T>E<)L)DX%gv7uS4dEXILnn*H+aSlBwl^B?d|Es#Ev1DEmc3{%g?BdsXfZZ zpTFbNg@zLu&5>m2dmBVkbgkPEH-}6pSeh3V@F6W7x@>4e=TL5C4UI#d@kFFbUqE-` zeh-e-oFLv;*Y95_l`wT{dhdi7gWZlUM&F=#@{8s=JqsI`yiSVPfb;0i{4zG0&hIT7 zhK8e{bg4QjQ$nMoOS3zKyyuxgVp37TzkYf$CJ+R-wt7T68V30I0o*QH*xBwmqaT#F20r^A2F`2F*Xyejf(B6nN@UO%o*RsIr+i-%{uA zDjDb9p?|e%TUj|aJo+g%*2s9{nYNLE(O8`1cgx?ogExaZbzZaf@Z7vN%_7y?`{kiO z5fHqHO>x!GJZ>0V<}`konNt$ePnc51lZuFjC_p2WSnmjg2t)1qB5s38XLT zsENt)_=ub|Nud+kfQM zte$3T9O(0E9o9A5oy?cnu|nF1`vcJJHkOzg2h@R?gZ$U!jwd}%iKo6~Z2M)Sa@c02 zZgja^*Ez_ET|B%(&dP(uks$mIZt~JCZHKD-qAvW@5J5rF0Q6Uw@XDk%LX#Ys+@Vlz zWY(QgJ|S&C^kv)K@d^R$%l=_j#z-hSE6mtBjf#S#VP!m+@_=XFgV3`1+~%i0L$k#= z0s>utHs|G@K+)-RPr2*OB11eALsK^`^_Ur{01~c27W^hCo&`5^8)>=>(K1~bnlLA{-&^l%uZj5TgwMU< zjaz)TZzPHo=tZ0u5if+K8yhQ~deQ|lSgd+~<#+G*)w7LcZQg=1rpEkd--Ox}`+1e} zJRAM)bXAjv%I)?e?i$yj)_$`0RE90XEIvZDFUG2*67_QjO6F_0S?EB9*uJyg7?#?k zs+RDQAEbfF1beXJX#-uO{G9ybWUuL}K(G5Hg;o_lyIkQ$_vuCtERUn!kcJzdE*^${ z3oU`Rg|TsE9TkzruiKGI_)(DxBqW*FX!zb!JIc3D*X(Mc879!!L|C)ybxPfJA>BKK z&kF#wl@J$e-gI2(Vi47qb{aS>MP*%whJPtWb-?pnTW{2MCX&Ks%KDKe zsTx;0b0VqiWM%1?fYO+A?W07c5DrUbvXpePx|=86kk+c3`t>8YI5c)K+zV%qhK*RW zX^D;s^8RPX7j!+taVe34m7(&9yIIdH8S9zzUUA^Kwik5mzk3k*o$ine=IS(G*HnD~ zpn2+il^pT1jCO-um**IPC7mWg(XEcJ)r|9vQuhqR(>*XS2%R5uZH>cuTUA+^esX`+ znG=6LomkKpH`FhgTKp<;dmZ!rS6&L0=3s&lVf;@y*7@0Wr%CWQZdlGKwm;M4@(g939P8Zjh+XGy?P*uMJ8K=@3ZdjokJv#n z<2&-|Pv=94=JdS#rfG)hb2m6P4-jtrhM&ccSe_0_?{jyHw#h5FwjrPx;s$OUeP2zj zE@ze3^V`F6s-IG17VD)!zKE`BYIdbcbZ0x(xSHhTkdxLt{cZA8;>Fq&s-GHXu5|VH zC3kvecJ-BGL@Xl)Rj-2<497}Tu<%XD!2zLxdn>yxt@eW$I?dn9vFkX_xLjRI($;5% zXsj2!$?`6*q$d3*!`A0Q8tGF4z6Je38ZWnsI2+PW=$e_F2F0_YAhanP_$Pk=jP2NRUMmlZ*eoc?2uG8A`9A(pjIWYx+!JF}z z5pWwAO=JT)WfY7Ye$u&cTc_Dd%f{%{d-7^(Hbs>KS$0zyb?Jx!+J3);)xCa1F7(=%lCyTZ;Dl=1W${|=+z)@> zf7cC<59#Hs)VYVv?5@PUw!{bE*8+oWpR!Y-hIwy~`5Ut799{69j@6sS?4}L@92=82 zVR6ys1I@UV>+&^8bDFH6V0+Jy75qG8SU%F{u?R6tn-X;odvw*VmS)1*bNPgXjX_6U ze7JMk?f~pFEDsG&cIOUD<{8wB`Ceho_9OLVC~Cw1`KGIy{ndOt;+B{? zPkg^6GbW^FJb%2ne1SsyZ(8_4RGm zpaYkaR#x7;=ZzR0uY&eqV!iyHIm7Y9Fg6xIyxT1ko$`GHL zbvw@_qtljEANcNh>21&FlSz*$lx=KKrDUey3JaMw1JSusNk0F0FQIQ`Z3Cx5PBE`Z z@Wj|YwR~0RXJ0l7w~=J>VX!|SX!1gTEhRO(O|**+jw1D`X?$LnvMFr35Y-m<5NEI# z+DeW4`VkN|Uik~}T86eEEkkB=4J$?Cd`dPVssPmg{KRxV;N6?Cf3*OQyGp(QqTxnS z)aJ&mh^6I7pgPc!(C@|m!Yl;r{!v0GGh+PKe276u69rKFqLaRVIHa_r*N#>f)6Xid zkRN_5kHWmYyYu@ERX;-R;>7-Q)Jh;q_3_z{A}=41j-Jl+usI{$(-mDnN&mw@{dQR< zdH%A~+LLph7}ARV3Wx8{fQym2>zMzy{SE~TI)u~>Lj|A^G8+2fSviD{J8VI!bqIYKBHAk?H3B(#Foh#GdoCW5K~^xz`{WEyY%5kfOJ zM~IVM2B{rE^XKHM-k!kFqKgHmS&Bbn23rDdY6f6xQoIRqq9O)qH<+D26a{7F{qW+v z1Y?t|8n*11dg1fma)yV0Nuoc8{y%2M#NC|iyZ&6~&vqzaf+@t}{QNhU{BSvEg^Jn! z+rK{`Bp}es{X_A6#zy-;vq1k>z789j{pUIeKjGv#hPLdp^2+S~eU|&G2`SK;JZN0p zw+d*|H^mjVKgRfGiOO!rWxnU zr6+@>SSgr)kNG@Kwr^kdJzd&fX9oG%$xqONjT$tC-HD`Ar)9bGAiz$C?d@>)>P($U~+C(qG}Ye@yT1@Zd_l z)G&0MKg=8zvh9$(va5gw zqRqC?tykI$#eMFMA53naD0k^Gx4XHR*Sl^o81+Sv%!an6wq`#xP7YdQFAz=4pUsURjW$|`k*^G>ibXx45ib-@L zoGdLy{#w4LYc$hVh|CEkhPT5zCfZkf$tedkZ|rY_dR9bOR7C%tNLI+;)Za^>OPOA+ z?6>|B0X&D0`~!1C7IdmUS8K#+=0k|24JPbe$%1Rm^`KJD+;b!dhdQ2ju^N}f&2DwX z0rAzvj^jSF!{^|%=_K+UxP=-(k{c#YMsuNpYgIubl%ZwijFeEx_;QQW!!Hl`tVflU z?2eBMs>P+HIPJGO@Ah_(AgRq{N?S{(9XpspQiKP>NSD`>?EKvgxERQg4f=(wp)CyI z7p4hJk6Ge5&GRGDdHmZNmmO&ql@3!LiQq*nro9ak&Kzt0XW{VL?|Uq5ZQr-ptEt6P z9Pqw5bd$SIHy>8R9a2r3J!XE)mE*(GRiNIMfKY`41om5T~@*J z0L}bWIecdVni8xZQSI zHe8Pu4EsESB|TR|`P#H4`pN-+RRgoHYX^?0ef z`^3e>1-sgDVDEUn#TfUDCTm>z1b`9>gCEpP@4g;E&h;&3MS?N_knQVR3Ls7osr>oX z?m+u4>EsQ&TBq??fK9h`4K{dTNF&9+^W4uaV516R+!|+LzM0DlWFpDry;2P7SG7cuSFp-9ZkuJ z9fBRyVb?70*-02Vz>9T%g(1;`ltIq4APcaP^t$2_V#=71AKb zZE}YG@&sM-#yvF|TgfM|4p=N|w6TkUqr8cc%GOG*HT{K2+uYrKJtk&(LV{g@9swxo z`@~;fSg*AE4V;D*VKEU_vkhq}ReM+0V3N3q4zv`XL)z>7Yir^roi&gf?$Je%;#pmH z0k?4D(+YoYTQ!bdqTP)A!kdzg?reL?Q%)}8g@#k|2m7PR9{ZR`nydu`(?^GUK7pOFmaau%f)qJ;CUD)*~y! z@omip#UWBQdHHXH0|Whaof7UHB@BLp?b57I1O4A&jaK#_l6>urnSV)cr$@l@0s@0V38ua8Bg#tz zNPDMtFJyFpTQasX8x_*{GeMRq@i8lo*I&vr=^*VDiMo7zOLP8Bm_nXnl0;0NOA}eq z+n&m%N@|weafuF|mmJLOEOXr-C$bXx%+z!4`_P&6$v$}1eRZ#DLh(AMznIF+OGBYV ztF(q6C^>7W7skpIzUzXKj07eBqH$z$`ELYi4{FVO-_*=4J$|V1v5TVX{m89 zr>Ha?VvDb5Y-y@%Gz@K-C_1VROlEZY5Z0y9Y)KrfMA83gQZ11oo=wX}lwWgpdYJjm zrj(2ZD6y=5K^BbsQt(8o7=14xuv6|!uE3&TQ0p`QN4yq8&d@Zg-f(b^U9?u@sgduV{RJN z=_ew1h4>`-a8_#t-Ga5grY1|TNW_;HyWfa0p4ZVy={NN&){wT`yxk;CAy?NY0(tuh zm_lo-Yc#!mV=kWJo;NONvT7i^^gkDWVQIL-ylJj?9It;3ke>nqJ5YQqVd#*P3(=r3 zEo?mKAC@N-Xna~t|B`=K%a6=MtfG3W)VQlt?F8oQ*IfRNX=o?obw&gmIV?;VY7K*Oh)C>V6OVCcuK_hT7r zJ9c&_ub+4Nu2ck0$uLzm|8SNT{eN{G9OjtQ_-$|5Ekc)t@>z zo;r9hKa0?LQva_y_+zmDzeUf_0||c3ifE;IP<6=z?ln-{J$W4Czp~zD2ITjp!)0ab zq02C8!x&I&vsPgJk6&?3k&U*z%@LoLS4axiwK%8#cXa?KrzGuI|6zX;oX8ojL&i>5 z{_R(Uxr@&u=YLC@iy}uwOE}LRN{BQ@#>oFDAPS?Ev|9~g>}#|tIG8h@;t9z~5n)## zQEg2TPSdbB|C9a8GKiU~5ujE_D-A5!C342*Py(g{LQhRfHx*Ec|FG!Y&OepF@2k9m z3d?b?;&%5KZ(b_)X1oMfP zk1n`~?nN@FfzqM0*DK4vZbM1LcX>I|ob7Z4U z1%AsXms3-d?!4%_c0r~qf|!l#?g)y59p zp#E&LXkpH4bCU|BhEhBwbu_!P8<(RKscqjO5%0Zw65dA{*FPsva&G!gqS(>~7y7uC z**`oiHFJLR$!DeB{+ru8MMS1FsP-*QYA;v__e3}|rt|3bWaj!$7>r;!bvFy^1bg`UU zZ{Ci>^Ka8FpRy(9Eil7}#l)0PS)jra6JaIh7WOx%hybBf?^Un0l8~85*Q75&-Z-0k zhLJm1zw21gnPPu({B;2e-#@^HAK4QsU_g8Co`!W5crbHPswmCWh^NCzQ>Rd3VXWhkWh=Yw*|oWAGzH8x zu1^N)Zcl{dKt4gl;r0wLqQ;_1%yx)fp)dNZlTI3J2Na~0jgFE`eKxclI9M3_9@0G( zl52Yc=d@caN!A!A_Yj+{uynR$A-?AF-iMdDxvD&BtS<2oenEW0)b<#NW9B?>ZVXq( z=K!&SBjZl5!^QUQEg2{o(9YFn5*ZLa-je6$a38srI(Ox;20d$SZ54QWcGBcpDq(Ed zB8L}(gn}X=E;>1Ls(RGbpr3oE^@5I;mX?GBFwQFna~ktdA&-FD?zJP@-RqQEo5~F7 zn_TGV>FIY25e$BdLfX?hhjY(6ZxnhW9?$Wap(>CFa3u5CPYd4vOq0i!u*r+=up?wn zKp!?fjC&g9*-SwJC+2C2N;d>?b)hMEcrPOg*u(a2HnRTQb{yf?7IbWE?3dUS(0~9i zG885#w0OQo-z1IA^Z0{bcsS61@{ zl6IrlNWnq?{hS3r@#7LYF%zlv>U8di9^v14YPq=0HTZ2iFmfskvl}Vz&)qZ)WAnTTUz*ghz}qZ zIg;sVNXcD@urTUBbx<5o4sOuTp})kk-?@ttD~F-tvYB@^WyVjj`}ma}r5fLt79Rvq z*~b=oW4)q(+i?BcIq5p1$w~oMI2RicPxqF~ZhQFW(EcOg`jWY_>cw2GE5wkr8KGQn zVoJb6h}6D7b;pm_k?meNa^7V((c3dHSgNr)(E)|}A8r8P0*y!xc<&<7?bfdLv_1V7 zYWBE4T|na|FP2tv5F4IRKrav*w#S@nLLC^lrMytuG00=0(iFr&vtuIa*tGWncLRKfgpF9$g#8gzRckP<( zD4=FrI=YZhwi*RBywwUx9Dm3Xs6&{sTE-IeXZ=1*s@KZk5@TPq54BK`GWE?HcF%w9 z+D&@uT)qpmb*6l4+ty-0Z`K#;*~#q&-<-0B-?c%*d##(|)uztr1*B6NnGM(ZOA_Hc z-j`+vjDm-JQyaC-K>FEm^RT7FxPxlu4nDbr_o|u@kz51-YfX#C$b0o+NYf)7h^Y!(i*o!mNLE`z>9CW3 zO67PQ|2B5V{o%S8?+VjQbV3XbxF8QDS<^i_rOV+TePXMtcj5L#E_{v2pNXmbv13qB z5KhVD52ucdF6RCP#Qn8B1cOzFyb8_f=a%y)Ez>AjDsUrG|t&2WShUXYU7~Vp&_;-M$?a zQP^*h>!l733PkZB+#c?eRu(?LO7=f7mY%-d$d9i0R>0Qyha(#wcATkqX8K_JNkc}X z&(a;0NkYxKl@u+t;YYXr_7Jx>NJa}m&ss)aUfhS08MLZmjpbVNOek~`*1k&d4I|-k zuo5=*owTs9y0Y-q_SuYoXi5sZH}IEz zI@hafhL;}NtuR4~a%LJ%Ev=7M2KL0ROi_Yyq1dyvf62R5V~y! zv=L(k+(&2iUOP?6QfM?1?i^IhC1iZJ4}lYU-qrh?8xkoG17#RxeAw2xQWYO?O+CrC+vb`4Pb ziOGlLLpz;wx?iuIH`B^2C%yny32z~iUaQe{b(I}dwS2dlpu9gnjn`RYTR1*G2IXn} zx$8O(=G#g+0Q0(3ya8UXflW7 zilTCaV_e_A{sxRqF3Je7XHzrkW84vj1#BU&*|ZMd8kXKX(&-bGDABHxtqvXl<(3bJ z=@u?D+{-g*R8h%I)!{zra11W=IlXB=ApH@Cx%sbD?!D4M6rNOo=>nPONKDj+g=Brp zXF)*&z-V^%9|2)r3@3nQCe;9D^Wrs6i+*P`Ws?`K+7$Mj)D-PWApo?g{nl z5EF%`$hJqn3b2`8^FzZ=y3T)_GV^zT=4l+zY~b-;&thAOc<%SR0-4m5IbkN;>o&<< zTs(mH^k`>icBEZ26`j#=HsHHGEH}&W@X{kl7Cbvs5^7@ll(=(R#%#BAm^OP+xPdB&h zSrf}WPw=S30)acA<2(oS-nrxc8GI?qntx&2#N4+rgsaBbJY)Gn=v|eFt35f_ayTOZ z1(ZrbOY5i3Q4{bXyCwXCQr=+rr@&T>af zdA+@}KiS}iKHQZJ19pNJqI|SY<5Hc>+vRzFDh3Uo_BzcR*}8VJGJ5xVQ2Q}84Gmoi zFAWF={o`kjqH#HXS0x-62#4;b6UyK7R%etG+_W5Wr15qfSFG&B&CU{l$&!$e0EzS% zT1@o28EPse23S-K9nC#F%GH%!Ls~k=4iP+kc22d6?69ttBNruV#kf75sks%jwtZAo zr|~}Q!6Td0-6ft{ZKguu+9G*du`v(9nJUZt35(N)CXICd7+2g)M>Q_*@#V|%ZKeo( zm<-|Lr$s4&bP&NG z$fjO=>3}*B*igkkL(O+CPBsKctz=52{Xu#W{=Jqs106Y)3uzyy2yj1a51S5-Y?KTn za{ubDn5p@04{B!F)H@pU<;VVcc>!TWC%#746J8{QF{JA}F8+h8qB)R??Iv zrJ~|P>gn$H!X;S09z4qclDx}HU#%AoPtP3HD@}3IBj!=Z_c27My8Box^M0GqOLOm8 zy4_T6gn#kEY2hXLZn_038}%o#KF_H4W|XYSL4Wc*U!|RxnK}oMDwf$4z6ECou(eXQ zCui!$ofy^k=T9ATwfHEG;drw}MmPny7@dqRo(n7L4 zISm0OaabJzpoOu8$l+loKwBVdC*V03m3sdQT@aH8eyK%d{(NudnHS7FcQ%%Ji|`sD ztz&3-Hx33%jZVra9_N!N&x|$d%57pWE4-wj<L^G$A*snb!RGe-Oc4Kf!iH;^ABO9%fdJ*`9KL9wJf(qzd$->OdZ;}0M(9`%eKR@V= z;_{yAgA4r}4b*>vwrRh${VV01)i`voq`vw}CY_N!tPA}W2{a%$7&t`4rf0>a)6-I% zong2`+2!Z;dGHa6Fw5LoRwkzU!a~g-4W)NpJ0^lr>QFpX2EgY?s7#R}u>^OEk=e_P7cpdnpUh?EkNO@vnt!_EEr47yn`3d_y zfugHyq}(y+Jbm0$RlNlvr&7~r0qv=yqYiw$Eb|h98BG>CBP#!D0p=calCvbE|52Uv ze*&zK&o7qzg%BA<#Z>^lp;>7i+oXD2P8S(_F_@#E0TIA=B-Rd-+*zkDT@GT%$bXbf zi*xxkP&HLWS}H2J6^%%r29FNMtxH5-M>MkjPw4!Q;W+JdrVrArEMulm4^|pXrmhNi z3HU{AOlk(pydWT0h*FMo)7?%%$M>+hE+_rhcmOf0SSFcF3SxBw6C?vh7U2JqEGCQj{P3d`$_o!KGxg3) z%0UkE2^-@`5t&r)o1}NvrC7LyMfMV*jUin3x8xTz3VF z?}B7vVql3*OrOfxVZ{sjrM>gQoi$P^Q81lP)|UIXs~rLUJ6n3>8k~Ff9ZX-~$%-|bxoz6p9TV@5JzxH~8%zdxP7vL`bZY%E2bl$c zRR~bKfc!DUS`08$E$P+=+%C)a$CZvH4}6^4rsq8>IZb z@gPlTs!TUKDe0T97BM%*4A7^Y?u=;2N@os5Mk+9$(Va@}-pw{S56#At`oGBkz--el zNcX4@U@8tEkedv$@&jyt&i!z6#P}|TbD>;+1!Qs#A(qC!bgJBE+(17=;ck+re`xa` zx^b(psM2QO8=n=%ndB~i8wqQXmU?Z`r^wwQ;GvPIOG_o$8{4VFt{$6EepI$@va58@IMh7|N zX%i~p`=c4lpec*<)|f>17kCTaqo?@J#36~Ln}f+use+DCp6~P8B?vSR-HJ$`)ZKp0 zc*~ROeK=X0@~HD1kJ)Q9+I8t*UI9}{R<*)pEH(5|SKaeOX3*0NTKtH=VdP6l_nDbs zU>8U#&0HKFmHWWV+4f|2EUrL*n5|gPDlQ7gW}L(C_gkC}7J7EigycZ9+e(-`x7iet z8wJ!j^Jza~5hzC$D?^BRT!rOEf%>fCIW71Y;(pp>e;<+d5Z1@UY`-~~?XG=SeNC-W z5}Ox(a$s7JR-%f58VDy!W5Vg!)kRZ@-VNtTk>;hJoEB~pwi^=+HI@Jbo>Zt6T?3ua? z$@ofH#|H_l8yjw?W^BH=B7@G1|@`|gd-BAf_tzTLk z?6Vo1l4;^Haa-Lg9n$OK|*jR zxa(j+gS)#!kl+y90)sQSI|PSd!QF$qJA9qJ&$)H$tNZg(RAIw(_p*NbvE@h+Y()-H z2Hpwk0X%`_d69phWQ{FZ9b;rvU1Lm=MMcrZR;>xUPL9WPR`I;Y#K$;BshHN)VJNli8oRmY{a#K;x;_a`jMSorTnbS zZmm*^+kP$OLJ0bObark^E&C`%TiK!)@XM$Sm^mlraA@clv;=KX24jgP<$)P5FVYHW z`=QoqU%VtUFxvf=8q4)8h)clGmuLNwl=mztQd9|7*H?^AEFB^gDMNBu7+93S%5aXa z8&jT<8j6JM%hSDg$F0F(NoY?7xZS`?>cy!UNlzGHy5&W!>XY6s;Q$D(|7s1t6uGr3-2#W{=eTVi0~ zmH!$AnbNjPo$~+r0lfP)2=2eX2ma_vM$7a6^PeP^U?}q0=1#<|_~C&gq)$+{)i;P) z?43aYZTGd7kn!EW_At*z)~=(yFkKEu6pN(o?fzs`a+uyeh0cP&>v zkL}xjvP*=PtyM$GpIT#d%aJMtg%g}sB}zR^ex=lyVaBkU;$iB0`_XdVsB+bnGD4X6 z?ST_n5DgcntsDx7L!6*Dq1Gb`r;uGqkI%9Hqf@Qsz`f4ULk zRXGA5CLg#>d=1}`_&;46e9-<0hos3&wmtCw^+L=dGm&c)#jw!-=~{FgS{~*0AWwAQ zAku1Q)^PuN9`%2EUYX%PM|A}|hPFT$oG~y)y^<2|FVnsYa8;TqhW+O)7-M?2t^d=V zM8?LJg)Nr+ap@ZOC>WK%JyNygpd&A7f1PV}iyvN@;jb#c|A-k`D}f+W@4)0>&ZN2c zf6g@tEBnlH>BX2pQ)Nbklp-nHBT0l2IjJ!ly=vYDi{9_986s=iIJy`NT-}^ z@qQPQmCo6)>T>QUuA5uf{ojTldJDYK+D3xdqw5FP!z7*C78m4&X20`d%Jy-C=b}(! zG2nO{#j!ptcV@fH5(g~rmy$9zuuP=fS()ihn1xIQOoNVCLJ3}UHSGFl6b zSdpz7D!~U5>R4qll>+#IYt1)vMKr}HOX)<{C|8L?c-yC(Dn?Q%r1^#j2{Cevh-K2TtEVV(S$`!X99gq1E)Hzw377xzuzKNgO zgQ|jHyeYRs!Urb4o2ME7{kA=!&y2of)Ic}!K$P=e z;wX?iM6{l;*olhWRHZ}8==}e#tx__}BLy%qh)HrM!%CFE6PW&JjM~q-<6s;*#ch6- z`f>O$+?%Q#*1WQe=IU@S9emKq_$+*DWyASC9|6RplUDCkONUIA7ohjT0#P94du%#x zeb@Z>fK%D8F^m|B)VyGhUsJ0^vWCDP(P~=Fqy7eMMk0jCEmqOCr{_zOw1dg+j#OW- zLdM6Vz3@nAGB9!+dmdp#;yQ1`o#zr|3m6?km7kNH@68M3(LmjGsojsA>8`2JV_G2pjT6s>D$8XsE3v{Jj|}-d}ew$S&GI$2JitAcfRe zJcAbe!SpD4>x$bS@mDG6z7)1L*?&Kp_d>+;UVZk*s={M^UEg>V@{GN@tOJi$dJNj7 znmt5~9;7^o2OFST0`}pIN}%|@g7a8)pIbcs?r?l%#p!-y0fCzN9ilwO>wf4yF7_Lc z2UR$PrJXSl(lXdLwAxpaxX}d&u*l|%P_rb}1erQh@gWbgaZkDHt1ul9<6q;kM)+Q) z`dVXW`=2?PeZ>Vuk$tC1=iOrzY!srR1y-`i1;S6JE3Sv<&if@p%?kS$G0-8C3LCr;D5AwJV9Os9mD+^+{Ms4%F<$P^>fOnmQg=q-W^iJz zVEJWlG6O7z_^wv&~?@`;WO|hG(`rQe|_91V6%rYoL*nmv|r0^H|dGTA)t*S%sDoE z%b1mEkXn~V_qZ+5QQ%)&Zn^x&w8zie;C-A8>BQz;;xGK0ei~W!^t-UI3h#BE#|UXf z9-&Nc&c7>hBCW;w-GThn!aEN2SMRYzXDTl6NZzaNBj~fYAlkRM*!M%^jx+eDN^r5m z;Xz-2#wge_`wQVXT25$rqb7JtRsP09A;#(NS6KlWS~x`ZGVO#*u@#XIq`x~DJ)P3X zRxEWfEEeK*-Y?8+KNh~FM%9)8(k)iE2q?>+elrxhy4Fn@)gi0n?5wn3Jc$Pm7?QT` zfPS5wbk$hAIX$g1FyOj4&O6@260O$@u&)KJnb8or+iI1G6~T!Z>l|hs5;!;ch`|Yx zT!dTEy(mVNUxd5vs435*q!XP#MXg_mfb{tmG^E@y*DxD92>utI7sjSaG9j5qjM4K9mqq zdR!F-T&6+^bac8KA`raditmX;f3*|?s&~`UtSHbQKsf@bz1kjcU2w@GBZ~CwJ-^6#fv*il81sBOn!K|ZCQJ9q?47KL?cGab{$EI z_3xYkw+b{6Rkg`WgaI;zTbaQ$r+|1pCo}b9Pf_AYRyXM1h|{?tYxjEW05fI@S|)OR zlPrE0p zyly2T2ZDMl;luaBGc&q)Ha((I_<&b_S4f!Nxb*UH6Z^HnksGeBty#~_rt{jf8&WJ> zgt$NHAoB?FW(Hpp?c17L1&eiQd)8>^wzK+ebt*n5eQp$wC}jbc9q(c6 z96U_>Gtu+&r^6yOM`QVAJOtKlAAIdK@g+7YpwdY%!n&ODBP~gc$QfQte%=vy__geG zY>eaF?p&+G?$TL#7bHG&-12-cEZk7@a_3QenYPUJtSF$6;mNe~3*KM$-PXxl=oFDn z=$D>^C@QcqVdz8ZiU@ij#XbV44RTLo;^=2_T4$VZuW5hy{cc%zDj7r}?FTsq5u?OQ zxA~0HntlGtOL_rrRA3h zuq?3#`%@*uV?)oL&$Fu81VB}kq?+bw6vMMiT`)S0|GZ;@k7EA(R;=g;hN8uN`$9A; zvg%zZl2424>Zk!e=kVdCE$Q3r)(6qH!YxT;OQ8G%%1Vu4UuOak#`|=y#ZSOmNrycD zF~2z}ek?mJ8YUp)PJRhy76*8$8=Gk?F|bhU{Ih{ za8CEQGidRu^*D^+%z*1@L3H2aG+xC{)2-;(N*E+y7UOr{7n~mMJYGQ6oaP?-dl8RQ z2p1pEQ=?Zz2 z+8c}~tG3l&DcBG`g+wAf=?25V^P6}f4wN?_`76ueN3RYw4iymZ>9L?Kl2&D}l(5I^ z!GwX-jqO6ULHh_LH!Bb*HRzZZ3S5&6yWjNG3_7Z%UK=M3=d(s{`@q{21A(^qJBEEz z`LYK#49{Rd-tliuGnm)v$MPrAo}U^BMtfXy zxQ<*3|cgjP!eASM%!R#6sV=dkZ1IA%iQ<<$M6CD`beip+~9}t&S*JvC5zqprAigN zSouabcf#5ftm@j=R0oGL%n*4dUL3l3U;g2!0~-G|eBgv+J)S*AzIQFTkx#qvNW=Y> zcK}$oWfi4$xp3tbnytU8A@`?mxRrE5TNm9mR=#Md?R)LxA`OPCG5*~qfl&_c8A<%#RY%y@jfqMe1&+Ek*mx&#Gs$+cMZJQrzfz~+`PuHVz}uFk%Q8OB8Yf#|zeZDsayADH z5OfT$t1HhC|Ca6QY7Vs(4kr$ zYSAQX`7;jj#&lf+QlYH3iidz*X+YRvw^lMH>YyTyE70-$4IV?Wc)e~~LezmOgKBd= zt=Q)o@stR)Au6zD3GKQEQpM-7!h>TI?6>PC7dO$?IQQm`Zb32SDao*9_GD5W6mRq0 z-;krAXswP4L@o)x_p{sChXK*{OUmJou~ci?=(g1-6&!TbC-n)tlPB&uTxNR{up0PZ z^VtDf-h*QTMbiC0zUx_syB8OitS`RQ2NMb`vF@F9*xJI`JAOi=op!Hx43@8uwQQlt zboQ+<+8Z}emDPSwh}>LL3lFW&Q{C5V)$8Vnqaz|cVW&)OW$8TCQ(>RYlO5Gf3qW*Y zJy+mA7jsN55Yy&(-U}?bFu%qd`@(w1jjG>Yc)A>l_=k&MFdY=wQWF!0R z&neHHZ&^0BpA1z~P5#_QU+2YfY9t>C?~c=5 z3T!NfIju+|sMUyahwC^!N&F6Jbwu)GBW&74N?GWp)Xf_CXTn!f9U=T^X zPmnXY>oZ)D%!_liM5pgkd1A)#qZPXrTU~kt#fdm&9O_Zi@vv~fO3?NI#8Nl5-y)Y>U0sob{>;p zKJF&gQ@_6R+#8)_cl1}V4xw(fCN9x+q~JgM11{x)$jbl+b(bfvxPp^mXxY4ez?e&CQfdxNaSdrxE zIQEhbQ}gUyT1Fbi(VVkk=TAyfZ(*;evVzX>q1qIEDant7p<+lk*S0Hc8)CU(t))=* zEJc1dixe>9=rHTvqPa!q>Pn~3Iv^e*6N(|>w8>GvIz3DNmE7TeBCI;Nt&xlmbSWm8 z(9dxu!s*TwE9_BZw*v_&I1n)j>MA7#3+vNucMN@}r&3;;x#9pM3KUGY?VvsH1 zhYRZM2&Y5NEeSi!3>hY1O|><*#=&{ikP54KT^mPY1d0=PK#l!%d9dr*yYb|Y8jEIc zryMBg%uZu9;1Uo#1B z+GGc>soHgD^Xu*EZh8AvMSD?5xL?nMTU9dqAIK5Wu}%{8d`6Pf0&~}BUy1M1IuNE* zp(RksxC?#W#1XPx@*nB_UTu0Ei?_VJADIe>X$z+;LPH;f#|X`Bdg%@EY?r*FvP=AGlZK<-)JZ-p^DyvCHTxZ8!%$467ZP*jmp-oxl= zs8W>2Uem8Fo`d!kZE@+#%yUe)rYnXq2FlW^`|fAf6dOG3j-^f;{~9d88PL+2Uh+D- z3s!o>$R=5=TqV(geJLFL9Dzwhc=y%{thI1%)lsFWJGOtD|$s?lvXp$+TNsWNho z>uiip+OaNUj8-=KDk+xq5Bye(^dl%eSdEC!QXk4>8ikODPW}lx(BEqI2Jp#-!$v0C~H|*S&336 zz=)K9FYaK)zF9G}X5CKx-Wg4gA?5_vhB2S}vBb;bp%jd1rV-#_q>O*;%{3Zrb)%cC zY|c0kiUCP*Qald^=Mof+hLYRha#0C=o3cLKDr^W%*$s)4nc-y%RlRp{jQcvlKY;KS zZd^`i=j?>ZGO{*0_aXo8hu7{#Ev;MhpvmNF4_)L39u$;Fj3+-nX6mKMZkylpT@73s z2aouUO`S)4HFgU&AZgjrCJ#=dm#gaGxzmGLhBAc!Oi%Pe$IEiF2A_*sN$u8CH`Om9R!TxWc(fI5h4f+UPWOc zI%?zye^sAo+%EBKV1Kw4zdXCuWed2N?N_~DtN;!__5*#G`zZZoD;+a@2-+`-@BMNy zN?r5>?qzXa#07tDa$tqB1&xnK=x6S{`+(}jRp1c{5_w_84smU6ekfZ#W{FN?9i zCvMde+&XE}Cp}U*3~7NI^;&UC{65%WVp?x+ni%9>#&@#ZHJMdhguQ{zVm0-j!zvGdzDzXf)rl>?Yf z38_zdYQA#jXIP2s+%nnYK#nkq@8tc_WogbT^oj9eohq#=#PbV_QSaEAz9_|KXktA- z95-;HmT(;Wj`kMvRdjWKc0W(~(0Jmyk2P6tyjQz7f2sMZ{mEUGGP2!mDbQ}?YY~fX z>Fh5oQKEeCV1i#Z1issS=Tro%h!v!Ru~^VyXF)ifqvW;Lq#iY2#?yqs{LS*A1^ ztcZxPDQx(&5K$1}bkXq5^FZO)>%!_-s65ru!gK{DM5`noFs`06(_ga78QvWO z;r?!NK!fyz^O$c97cqMgnJnoY4Y#5gDJk>PYoO|SDEaF z(zrFuN}`Ezg_RzRY+OV~6M_tFEjui?pS~^moy@Y0{O%s<>ToL;48|iV#%D08p0njo z%`m7kpHiK9&4$&OXz(3KMPQ28S{8b5aa7d-iu;*X>w2m}6Clr+!D*TQ(CCRgLtK+9 zFG@K~tCPalV^J+)K2?S}yVG_HGz`T|OHvCKG&esHP0g5k#uvx9zm)qllKV|C6@Eru zDX{Sc{AcBo#7u$0$b{#)>qcbAqCAUXQML{(rHg=+4Q#+ScVU7z`K3r)?6Y~ZT4=a% z?3Z?OTAIn;1>~0&)8|K*QE6Fvwo$HDOrYSpAaN>Q{`m=u=~);dZC=muT;mcjO45UU z#IKbFO8(fipJ`L`vkxx`^!7@Pn+kw*%RlqQdp4hl(XO0F-XUi(ce-UWQ@1cBY`E%W z#rYytxMF#dQ=oTo^64<$ggG=?ne*)~4U7o>SHY@fc&WHZ?e|)b^qVFUX<)UdR*V^S zQ@lSY!MU3gog!GUfmI#}%JizIGFb2iwjV@`E9jdVv)}LR1&`4kX5gN^A0R87&)7$| z*PGJhSgK(EWN$a#-tIkC45Oy#?AQM2Y3g4|6axdX!o$0~SwA($l?@T6%Hr`3v85i) z_D^TC^LRaejMZy%lX{P@L^#pe%fdnAx?I&^-AD%HBO@Yup_<2N)&oMTn$xqP39*MJ zUjFH;sXP3Cmno{QNkTR&E7-NMy|KBy0SgL6d@%VWQ=bataP~KCJpUV-kG;*-vIVNX z@pQolNHQMkT1bj`u__+A0OK^RO*1oyi9CieRb+> zD6t}d`aObpC6g4uPj#cKHDJcN0i9*cYFC+$C{ryY4jG7m`)FNgq=Oe|^8fa|(1EeH6#>my0#FklWguyg#my>L)r zOo~^j*6R@)u9R_ett&kTy@lc7rplTw&b)_hulhaGFca zCs9zf@pCTURW%v}QGx-_bY-3qk@S^P4#v>Gm#!MjR#Zw{(2^3$4EH%#%T0%(sfHuY z6B6#ZcRo;((37QbZMn9JLkYGv1crkfn%K8L8ZVIYr(Nc?tDBVp{87Ow+IO7rC>mBU zUl)7(pE3j$j&&1H!@KJ+IkE6WIqk3h2#GzqYtrNL@fle4{eQ6wGg=bVpS((u#q**(-KGM_^?;9h=Y#~B$ zNl=pN^H<7zQVybe9^j_h!pC=@#>$3v?MO=-d_Ynln(XRYt0i+yT#pF4k?{F3i0jGQfQZJG&~C8HeER(H@dv^;zb6@}G~=9+05asX;2tIBYUC(PDNhpZp!}R#Gg!1l zz-`lKoQ0HWWLU=?G$UFti|6gxL~{4c5iZVXP5Jf)~&UDL*t=7^#~@A2bKuJmv`|d2C`*ty8Ra>03++v1gxJ#;4;T?E zubt8FNStZw9+zB!tXK^gR|ueRai`0%9+0rTyu&o_f6w-4u`2)jDCfsNd8!>xEq;0l zQObhULyu^nW)os-<1BkOw;B!)aV_s29fl7HR*Ov!d1A<%xP1w3TezCiJ&6$`!^9@C zHLp$RpsL+$rBBUH`rMkT%y+s#z5M`DOT=t+iC=vRSw0#sDoI@zDaI&KMHsjumvlw8 z9o@Rj7V@um*zQ*-^}D#W#GizkN|z?%EeP%?Jmn1*fA(!z($dkOi7HOzX5x}cTK@x? zQX*eaPs)QVhEgEoY^j5J6E@P5Bc_}CSQHAyjI zonr+EtJPKF-aGBfu+vQjY7P02{-c4KY*fx)M z);7Hs1lSyIcc2;lPEuj#>m9vj-3s_rLC>g;pA}XF?wUmzZ!uSNNe0l#*Jq~DL4ZuP z@mTQ39d92{{~n#ekamB8`xQt{SFzGj1b=3G!t3833?#v|9@OJPg^GAYQSLdi&5n<7SjGEO7xuMMOjrw}JEx|A=GV{iNmH_H-O!-(JIUn zd%d1#0M@`h)MVSL`s&4IOV8{RQce5S=a-(MCU^2N{gw{k4SaR{W~ZiP6cxz%a?DGU zgZvJ_1M-*eN%t_I zk)vhCd4utO1W=7|M@4_?LF-FTKle4NtL5r%h@}WX@f7+&nmz9Yk)ph??>7&}oSKa` zukL-lTxjB@JIQ!YPH>hh73oFN>{wQ5|x3+U2w4LiB7KIgSw3|cPE}TS1Fmls;tE*g08^7$m0FC~}VlPax&S~OjAwokED1;RL~6(pt^Hnw$XTI0X_?qrhEWeXaD zapM;F!u;idE7s>N90>4DMcWd4wzO*Lq(6Fj7ZjR9`7#KJuTSt#{u&DV7@p2Iz7d7c z^U+p(@5$n|tZR*&3Ru-^Zx^U|y%d2c(|=~bmpdZTX8p-OGq*H6+yA?F9{4COX+;gc zI-Lslx(*ekR3hY@Z-~Fn=~z)BT`j3Bdb~bK_j*94%>Vq}&5JS>kOtilRWq7J>%jzM zC23`So^d8nB=ZC~B>fyGPB~GA8E49H#oqYn``tEQ+h6f>Z3Zv-Yc=Ta!a` z<0p0KmhGNP`qy?k?qm+Gj$Y4CV+s74H)H}2yu1jYd6Kckq)2cPkMon}^-G$zHqZz_ z4E(EE9nhO2shuVhL1{FL03sYwpTXaNsU|owHblyC5f*MNwReyxls4Ql%J1p(5)Zd@ zeSIZs^XbprTFoWE`+))?Q|wK4{|2hcK$O35+(v~*!v*ZBu#Ui1;tB02~k$7 ziwyUnn%OS-o@Ja<2NTsCEw#erY~LAx)=f@T$-H?QQn5_mw;+D+4HQ$bhPvgIkRrm#X?b9rqffy1e5hC018oLC@LgGTqzr z>SWe&=zv)dDQ2ne(vY&WxXc6aNXjcjMRp>okrr*XQU}M)(kCHyXEOb$cScB7R&lJW zrl*h(O!`s|#1A!|3hYhQegOH%KiSyZ@9u1hQ^f~87hK97V9N0*fc=^z#Vn`H`VJYE2SRv;AFJ3a)kEo=3rf@PRkq08K?(s?bq&+_s9CYY|?;w`}#?(R9p&GGSZYkohTCo%I^ zinx|8p8yM~l4=~I?DzEyk4amBe5bw%q@luxx)+#+#h6muRk0L?KnM8;wJ_P2OGn0+ z1s&ET*J?4>lzwayR{cI`@)^_3z!0iW$NH}ZPEDoPS+4ieGxPb?whnP??P2kar$R)W z`D+0}E5G@2(n@5^+$zMW)CJ`gWw^Q7Y2Nl`mTcK&^4C~zMZ{+R7&qw9o;?Amie^vw z<|jFcaA;lrW@{cFiZDr@m&orSNWlBZD#yCwJ^2>JSwx8Z2Baa7Y>NGx%o(pyB274d@Jq+w|^^qFlG?vNURq#mOJ8jlvEp0Da=` zjq{7N5CNgrv7W&tcktyoW6=r$+A5Z1U;XFpXz_z`{6_@7K zx<7^j+tGYAE<$X?V2bCkT;uY1wI9$Zf874vyanV`Li>~bm{JNl)&}~2*%^}Btw=KxSR6t1V<*v!N@u&SgQ3wZP@8>1Q zq>}xdz9D`ux0Dqg`1Ti{9|xxvxyRp$NDAb;WA0lSrE=2v_jnN#ry$djq|jDq;`+Sz z-rcI$fa{)%c@eG79E;`b)v%Ap`yfCu@#wl%W$WMg8F+{!>_NH_x_gxI&)yl$XmRv> z0!j*4P@u@4-c`GC*3t-f7|=`(Hg>q-M>I|5^mf2FPVN7*AP(5x>)M`lZ+ORnRqwUv zbdUkeI5WCX?cNvz!dhSQS!wXS1$IR7JtqVeJJA9_?l8XmTHE1VkAEiD$3Nu!J^>}+ zj*&YGovWmj!B%)Gid3Mukq>pj5hz)*n+F`QWYQNu4%~yNPZzyjiU#d(Kfq~^Yf-#!8f8r`H($7j4tWv%tHBas~1d}>AzuFtw-dJClm;H?J)(3dtSufvm~&$ zMYpGYK7Q_>_|;w>VD>qde{S;Yull)#;YZK(gSl0ULzoWDbyFo@&UMfiLP!whD4c?2HxaI*&wlGCu?pK6~R6&0a8lL%52pY%nN(R{Ytp|5m6i*YQ_*)kR)1Ny^34+{-NOvcAIAb;JjrBBlpvfnYOEIgr8Gynzop9r1V z?O2*>%bMe%ga~*)h=-_lZ8%97#fwwDMGh&rVZs<6XkGX=5HHy3Zia{7*ZEjw;Wxw} z2lK17dAEONSe64I;`wjp(#nZ7U_~4b|B>GSgFX1?4+sS4!5@Rg-RDlF$0>t}Vxoxo zw?*t0y;(pcEh+FIoJBq>{$i)M!IrJ$;*E1kHQ&<`_gzms`|~jr7h_7kQcj=~De-(s zqW&{N+ObgdZ$jol!CfkEp+}NzVq(vYZ^+3dB=-;ZfrkTy;XykSwbfi@_|IBj5}9JB zCm@HQb}Dl^?5egOlKgo?<#7Tki6~-jX-}V=-HjRk<}wNL>JzNt^8Vi zg_kC;p*&dgk^6(|{STuus)q3Dw;+1l2%&ESFEw_}VXf9laIEjhO23yEmFg(g;n88C zJYtf*4nN>QyLi+CPj~BKI#8r*Cu3ifub%QRm2R&{;P8L?IPZ8q?7u4y(mnVkz&g%_ z{zs_;Z==@UJZPbL!S>6hJV~*lrG@>@jDD9&P&k|M17@DpU4C%|4zk4ipg$A$N-OAL z!{&sWJk;P*`lQg@qjB+shUB%|!($WkAxgr0DqV(#P?Guj`>Wzq@JQ<@NW=R zy6VV#oiCqUI;^GW+^Wp(_zJEQVEHHy12z`Y-YYe#dt}Q)42j#d>6!8T=A<7 zaT0en!8lmdY~m{lR&V$qM~N;Q*9fEn4dIXFkzW3;VW!8@lGz@xHkUqlPtb4AtWkoA zIp*;?&V}iq%zzuCehI?`9kXDnxG(yatTXPni8mMPTK(63fOr7Nb(mlOw@*dNo02-; zQ&j^IBz;xZaci8N29qW6X-WkJkIJrI{GV}0Jwlaoyt&o@*=5cy!4zeCsK^qivVRJ> zQrPR1sTA$!Br2H*n4;#puWeiMlLU8+_D{;YPPtKVqT>L7F1<;$w~N(M$%?<^cULrP zPTTH}pTbRgKU$dB4(*(QC>Ut+^nfI4_|HPvkDD7E^y}A-xBGi!tbHv| zKw?tKc`oi-m^S~pD0z9n$Y}M+DVT#^ z_`^F8VKgKAS%l#{EQk)XXyc#jDiT)!z)xW%D`5h*lqQj3LmR8-+?3bCa{d&s=nZv0 z1>!RR%}{fPZ7T&xh)4mq6X~ywi^d~f06a05b5;YaARXcVwSoX!Nh-RQo)f;EL^E5Y z1AVNsQVsXLW;%w+p$t)R&5Dl(ciz;3gMr1Qc z2S|#f0Lh4d2A6=HngnpDLmQ06rH$`A89;`1Tgu_S?*N=5F8f?N0frL%jp!&~;|r>q zV(~#l?pT1T-HaOvDAOYy5tJuNQb(dzu*Y%ncaMupw$oT1dxIV?*oqmTS-vpS#WtG_ z2ilQ}Jznn>TR~Lmf1ZqC{uGA;_OJOLAH|pGx0KPF+tLAMQ^lXuD$_WvU_oumxaeWQ zSXRX!w}YW9kE`~da+=(-n35UCgvP5lXKd%LmJUtk6qCPh$;j@*56^2dl;P4EENxH( z%LWej<}L#IvQdMMcOY@2p(ngfERiFv&OL1xrrH44i>K>^-AXtSIr2(|t;~A^Km@_{4mpx5wJa1`QGM(w9@+&eWc5 zVxqmbuBLBXgBe9jI8>{I;&jnn7%z8h%+0XS3;DWzq6hwSWYMmvEzlpIKT4pp-2^a7 z8sQ`~Mu8O?Q)W+9ZEKo(-aEx}YgD&-#8Pp`iwjvq1YDOb5nqdA7tq1ugB}Rrf?y@i z%Ht*OU96y_K@f_7=+Rqze@oMEmR?PQV=d8SJF}zMSv9NoJz!B(HP0Ce|ESIV(t@K?cb*G z7zjZwPcn?4D72ChqObK4f#yhTAsd;_NoY=)R=AKa$iVta3D)^*`W~q1>n;vS|Up~+k)d}94{8-^n^`sb3OLAcvK@XCWWBQKr zu8u(C|2CS&tU2goLx~SUC8Qn>^s)A1m&!Z;Or8wjPmWgSrd)1tjy2z54oI z1hXCJ%)Qne{;hsyC0ji^zu|M-$$cnHoW9a|drKyz>vw)8xmlE-!dBgv;UVx-`F;9F zv!?jBdA<9E3S{oU-l*6~q{t;tjt>`Q5{E#c+O zU1Uz}zIM2=Zw?fc7LDuGS*;|q6R@1ldrQw0y;l887d;S!5n<5;ob^@O!)}qum1>Ny zq)}Nv(t*qg-AoctK+ncc-j`AUQ#zl0dV*EM+1k-pfH4H}=Bw>DXTF6;d(XAYNg?*c zN#S(Ewqf5G89tVM2ewyHPpr>quwQI2 z-%A|=q9TD^6IXiZ#z7Q7^xLnhH#A%`i2_tN@Gop z2kEVbuk}CCrH1tf)axF79#?S#gVV8VXMp|iTc|!g>o^`KU#1sQVONU94A17gGcNh6 z6{4x|Lv;%7IsI^dUzH(oU1V*%WdA^?;8WVaA~}_aSjLfvApZ^E;D8Cx%wE7w+ca`1 zEHAMmp7Xli#?c;^HYBz9tz?rw*nyo;e(a!3v(n@us-$@f999wIW+bTFoK=79=Z#oc z)w+)vO!hbuC{6b#AmGRa?*6D+MXz_)XD_zI4M*Jzl~%+N4RAog0VI>khE%t$Q`cLn z?-9BQ}Q#}4KIn}*9y)Xd-0`S+~HowEI z75SO+@1rkXxH4=&;K0*s5-oC2&&G@Uhq=YyurHlyW`_uK%Y6a{r^3}>Fzs25Y#N&FW{b5$+oANClP7bkc0i$LP4NXh$AEXyRzxT9UM#QBwz6}`^qj_6bVd5w*iVJiuHDRp zJc7go>-tcKKTy0virSi=>T1Qs^`$)@_Sc`!zB)+tWzyqjKTiO4hR`Yi(MJJ8`NqM$ z9P(WS0pQw!yAc8#e>R#Ezy}C4>U(gOwr~U_*kJ3m_|svHdX)#utAq zWTI4c&@wF1ILU*ndVGbh$>uboec|lGR=G+9_AbDht4O@a?{yd9TW)8`=&-zua)TW z!+q<#1d{I+?&9_~PP~8^*#<~C3~xT0e`5MvdfgZ255? zTX6MQudArotmn|(KsE@E@h6?^}z~A8qsi7c+*3WpyTJ?f3{(u9SSu# zcd>oit?|IQHw$w3FD{=DrA7cNd}}8wwAFyADwGfz)Uh37Vop+m+jrBr= z@~Punc~kj3cm3?tjPeM^9hgFV^cCZ_Uv<9wvkJbOwOg#NsQB(Tv+;$>4;l6FO5k7y zG}VAxTO0{%eXRj63?l8IbH-xB=^Omnk%>-6P+8n1^fFvtL2(Qf{d6X836MhaKHbUw zFU{^acUAd4Qt&}0uKtN~%|;Mqyy|=Jzm)w;LnMHHU#AI99Z2Ko=1D6Iy$Cq^i?cyy(PC0<9*HkY+0OC1$t|`H29;eW_LN6zJQTV+O$Jtq=eNlAq$#-^ky(WsziLPS%)9%6 zNPMjD>ET|p!J9l&to_hd2BfQ#KSjQ0S)pSJneF8zB}3ci_ih-cOM<$MuRBpnv3P50 z3Q6(bU*xQDI9jy3UW4*RJcxVYr^EevMA^Pb0ykl$5S1%Cg7rB_xB3zs#m7MlT+P(E z(6qdNwjN0#rL3r^pyvzI@JEsi&YiO-=t>q;Lah-4@IN`~J?iZ|jSN7hr2kslvex-| zC*4dvJ&j2{xid-oDml5U9>(>Z^Wz93&L4LP@i?_NiM|FQ3)ja_Pxw@#;>lk`c6=ZY zY=1sDpAfj5bX@$o5RiJ>e4>h9&rADo?I&~ty<#E?qC79Iar-mm|9jU7^v0|6SV2YY zO|I6>z$8oYIQgbYNyKJzcA#O9tMnUi*nsu*La~C4Wu)Vt`D<95of>#9Ihg%!_AfM2 zn6eMocP6dviDs++Ub(Q)Rm?YSmV`Ti%rfy_gciD-_p5PhFBi_#_TW5<$1m7TCFz0&Xl(sCNxn5ehh;5vA-@3 zXBnG+dA#{GYm7w2ZlK+gS2D9fa~I{R=HP)W@#BH4D{7(GrEm=i379N*eHFf`VIBv=E9o4zrA$kXH)n^$e7Jz zyD}UuSr9Mj4g2ueH3rF5E(2Kk?Dk6MdsNag`;4((`NAS;CqEs0!vKWO8h9ong3aK! zzpd;0a<{<&_+RGj;kQrLm5iSTFz!VJoTYzXW8H+UnbI0Y5<} z-HMsx<@43XiWCSfqcUw0pro&r;CuSi?`q#s=W{-?GwqI>~C&_dbhO;mhlwHR?S+j-0nGpCb%F#nZxZkT=M*Q1sZ7X zFNF3Ll4|$>Em*_hEt+xB=wz=*oWJHV+#eed&E_oE&9l_ESFf2ny!0Z5QvDRAOxIfx zgg8eumt-`T=$N@4ClQsgFlRd7{hWDaA|)C05}|wY<#z+0N5M~Fzd{sNTTYlhG4CaK zAq2pNb?a1{tZ$7Gd*6?b?OOjm1|KL_uVDxQX`iqJ{^)@(bTT>ljs!tJm(11`7GpO* zAyV+g#2phugaW)Xd#>~;2PZq(fIKUm^*$ z62$U^0zJHYN0$p_U{74loIs7g3{{&j{?D~KoH!;!DDH^_pLlwfH0!}l;O^ke-6t(B z3cDl2IhyL^WuEb6Bw9K*iuwwC&!@D^MNePB+&}@b$}7OGiGUb9g3xVF7I`lJQBJJz z2E&Dnv5uR5dnrY3Tbie3a9Pj>X?UYy8ZQ|%F<$5BUsLCua%Ch<|{{a=$J*`)OBj`Jb= z0>qHpTMzhv2}@3{($aFaZPKN`o^OnLSXew&@kT5;v5*ppIqoqnohv41_Q)Rzg%C-J1a85M4z2;A!c#B}<~V z6}9^^7$INl819-d<_nQ*S(dkZk@mmmznI2c)HPsZ!DVpQR=m!EyoG;X<>TOe&3RDe zih;y6*w`eGeIkV^cQ9EcTBf@~j+vaO_R#YDyH$=Sc{pe0VS%M{!4-C6g_4G~$Pxgm zF$QA4(pj+R&Fx>_Vnkq#=|>RaBLa@S`QJwM=$L9a@lk2UbbVY$q+(AGWjf%5w*oMu z0v@lRGQlcI$?*xv@o#x-95hz&CNtt?3c>oNVqhO|pgAN>>8;;(FTNrB3ZwsdM-PwR zpo;Vz_MYOeCn5#R(37xHUry{LFSxMERO(HZ?&wiRIeNk*tG=J{kHj~#=W5`vZkpzE z{LTtq+VbeJYza$`b+aKLo%;vR{CfQLF8*Ap93Y`yubs5si#%6Y#-M z>|)QIae01U0a?0xRP1_Epv1z1KsdY{UgS!Er5aY*!|~8gmZ~sBZw2UGVo>DUH+9wCguH_DxxO=qMXDQ-(kgh z^UAKb?2c5Cb2Aa7$A!~YS!uRa<`Lw_M!}8an1y4dop;~i!m`gJ-F0i=0|;gt3d3w+ z7wM4(sJ&AO^}2Y!JhZ)dk&S0+PxQZt(qetrsw`F^EGS{R>tf-)x%Z$;9NhW52^XMV zk-&g$%hETO-@M~MY>$cTDS)14+Vmr%v#P$hXJlbzvS3f3Ouol>(Qsr#bGGR>$7G5w z~@Gu z_9cFKBwKE;Uo>`==HCy(xjnp-AwfjD-;#Z>(0IZA@vgtl*YnG~|5SLzX&0H&Z+uvc zH3s>Y>QJrg>B__}gd(7Eb zIpvV{8Me@pfNtk2b~$_1>em40Y+^=HbIDIQjl)JxELmldxX%8Co>`d5{ulC&EZ#?2 zz$4a=tzB1lGB=fY-B@q}>nYggIdTD!+u~SvdWqeKN^Idm)0! zKx1DD0NDwiD5K6<9e^!=N=8WEFWc$Y z_;ykKQ{U>92+)7g(@>gn|8)KY;p{MYgL;3*dURmg6>~JJmh7;Cb z731KY_F@9Y!_}4L(ZS$kCBi`aj&o~>1c^u*mXOAE(!?G3!!wywRna*jjppI=vd6}8 z^Xtu*>}Z23B3>o$7$CWEWFK37O>Tca^}s3mhAUAo{IJ!~6Weq*@cF%63$~1@*xx^; zEP8!WD9K>6g56B?w!4p$1Yz-kw;f|_X*mzP`Ke7d&ZUi>z!m@kr&?a#^htExB9d_E z;8H(R58S*4en1&JoVr8`$C)E8sWlC-*uu?f)?VI5)Dx&KfT2IQbeeYZ-jY)rJ@V3D zhfnF`e@mH)R|e|xraJF8dJ(^z^^Pn-V~8cGoAkwhQdZLPgR3r_Gq#jai4xUFJ&Bs5ND{F%@W!c}O07u6+ zaGm-%Y8@3Ps3;EPzOo;E3-+Ot2qJMYMg$V<8=uM1|C2^42>C9dn6G^jkFW z*847-nkUtImp6Ad1EOh)mYK1*rMZ$x(I*MmTQ{0lpX*_?!<$AjMf)XIk)mbXgwaG@ zi?;_$IY)oIHA|)tx)qUPB+x;Jw@Zi^Rj=EL;Gg`r5VF__M=>26M*6BX$Ax03E^~Wn zLq7kiRA?SHPNAYm(n`EVyllbx+BdI=8iM;o%v<@u-%%t@A~Ws?#H&8d4Db0O`Whx9 zFZGRA*WdEkTNvZxE{;9V6pg)k_53HJth9W&&4dL#cW!@Gvy*jvl?bp~8GYCuccc&g zY0S3b>r0o?>!+mJW_8F+zdeCkM)Itun3pTzzxXmsExzQNIWZCtt1S1e-nzkSb6CVK znKZsHZXO-QF1LnvD6y!X7T2syz+2q-oM-je%Wu?04&xWK4kO{v ze{CEb5KVnQv0G|MmZx~O7;+DmFe|1F8TdaW_DBI@PzC?9ogsbFY6uCh+im|bK|()m zV);r1=ihk>Kg8)j#h)p9IY3RH7 zLHXDscF%wu4UCR2S0Z%u|CTqasfG?vH+j-WrSDl@VUKfja;Dae(I^Xn!|!Y~qH4;j zGPvCbB6wCYP-3&1w~`Qh3O?NOyB(tu30R*GRahS$z^(nA*}8f49TA=A{v+C@INm`&rds6JB<&ldRZ-}60vMM*%eYIzzeQDhXE$Q^f9 zEqp~+iAz|i3MN>cocF{Cgs3643raHmgzHb3Qi&u~PL@4QaoPEKXkV4V>nx(=afC#~ zO@t~5u;NG6bk=dZjQ_r#v+s8#2HcJo`AvTXt(%lQHk|TIy`HmwpaehdaeRCA;Dia3 zmw(=6)?R^FZxw32Cd0v$B5L+B0+xfwI+oqSTu%&eH=Pf z)tMJB|GrKOl{95!vpz#H&4!tg`b>L}1hB-8xFE$2MZCQ}y7xfA{+F87u{!$V0IWwN z+DXyt0D#6N7>{#%&0aTlmFb_QpW}S}G^mkWtSe*uT~nv|l+!jg3c20)7&S|h-)#dM z*jj1tI&810HsJMxJg;wm9sth3p&+NXPlwYLC1QbrfZsZ zf{C=#8aOHHe_M&COh{8~Q8ZI&)vV*iWlJUipqG_BO;ra}8&<8IlfFZ|;62)yL;;k_ zRn@pIPi=1gUS4z4WVJ7D+QW<7F;3mlolxQB3|AsDFtaQM{c=nEj_`n;CSUL%uW(xa zBTPV{TItWw5aev?y5vb9&WC~N9OemNM*KRh)^faskt+W$Jd>-p+p zw5I8qJXv~>f&N**kMH;{?HAj;HIuXA*{o}%GExlf$YH_SpyEa=oy)4V%-43W>uWOZJoMhSM>+_ANg3eCADg9nI+ycFzK zO5d<$k=8a^>AxNkFH_-p$q4i&%BoF$D)*;(G{!GhDo>>6LiDFWG|5#e1G;Axl=D=h zbvkQH3m>{xpRgy##)g&`yFw_!w5D|-mO%btZ7a0dqSmr-f&v|ksM{YM;;vSOJePx_l`Ai zWO0&2DdN2*;OpLlaZdoDfiG(!j5JJl?tf?jrb?gh?qG#u6ki3L6Tiv=Yy6E->FzJb z7G67vyf5W8$x-}ED-k^XmX3baB73lu;Q%(YpqjxAPy7+o4~f`Yb_UB9Z3KYjw1)b3 ztzin~`1#cf?(3t6JMqbdjOIyh+grZd4Vf$j=yv_B05eeO?;qWjqgW#KIheF2tlx;^ z-J}!+Q<>`Mc5OQ0_XT-y;@8YRttorV=D#A=sA4+ZJiG^mITQ`(`O*o#fN=duA%AzH)6vY5$Ex`6E}7+aioVIA|v%aIUn^(?W^VZ|aA)nZ1Tr>N?r7WI(~IV9|-^`)S1j6~}Hhde@`9vhNo1=YJhIUXG`s z_Ak5*1&`p_RA*qj-yip_xn;z_iP0EEiiBH%lPeY;tn9dj3Au*!?+fAfYN#2>45L?A z{~j=uqVc9+Cgh41z3O|Ow_lE}U6HeDz2Ni0(`~(88>n@^WHreVka9-|_>ngpkW=xGElnjsw_hOL>3>up<7dB9m=$CWH5=NkPmN0V5|L1=YOk0|+G?m>1ArEf zW883YLN*(v4q;6-z1@utb37Tccuc(HsyR!cv)%i*#$Z~$K`jl%8( zbJZ`B;ov^g&fAl_A77DN2p_u@`N7y-YyLXV$dvuIukV6pO#An?!-jFl$?f)91KlPA zeYvru;(DzX+*7B~V_=8CvVr!w`tTl|c9mVv4=rqzw!!^x!v3dc}PKZKg!f(?A zXf*7+Va1H`&95kHJ7P{?f@`PstwHeuJKe++E%8;@dAJGW1(<1#&8bjaq-0fx}( z(=Af5J-H0tr}xn@O=nBOaWVX@>{0gEza9)nU9bXWlbicLK;N79G=2Z~#4so%U3Ncp znyh^GaLisO+T2c3bH-(q>+ot*rmf$;ShOrtq934Ex%eHd4n~hl)C~U}pNMx@?*m0B z^V*TKrZ+sQXAYtysiLnei<@Puij~Vg*3tZ@_U3#<0VE5Q%kJg|>3C!03fU2RL!zFP z7tz2>ntM+?>6Yw;j4jn3OHS(T$jH0i4xuz0<9(4!Tt1nR$LojJ1j_bAt1^$KQ3!)7 z`&H=Ajsowve3z0He_@r;HN#|nVZ90+vGdE1AHZ=TY`hJBDx1SdHU_nYF6mNUuOVa@ zkeuXCFrDeKvEQx#f`bKADn7k1l5WJnPL0gG5Q^_mpj~4YH}XknFl^H%P+F4x0=zON z)?Z}jxIqg`bvkcWi6SI{%Y#Bs^4=iPyasSQ_qM~Z0OO%#-oxkU?BYqw_puh}Z;^v= zKlI~=aG42FB2T(b4krL>Sj}rGsEIj~SZSM#CU-Xj95$tc9JF#pb5xkfwo*K~jZ=J) z3xB7l_zQ^Ckzex0=^vl}=hm{n!;#>OJGm0pt;;Yeqau#7MPH#^{ z4Us|CO;!%t96$0CYu+0mBwmL04LGiz*fR;Dyby`A5E~g0LnLoQ&+-5JU6n`3Hy7{D z`kKW}EPG1-!G|_GK6O6?p14E(XKH+Jh@_Smc39fw0agw2UysL!;`&<W}HxaLt zvmQa13aqm7PcQ0iOQ(llLu|Xf=u0#uU8Vh2;c9-`8l0{BzUe+ry09C7$i>2oc%_8C zODZOl!+5c;2dhUkF$>AW`=*^|oA|i|)n)%g`i=u?F^mP^E4C)6GghWpgLqe=t1dcp zxp7QEsJzUc^HI}_ToCQwUf2pt8){2dgHZbZuPFQg^|vw`A%HV!q#4HghT)sO z1~LGq0)d_yHR^wky+XepI057r!&*ku;^35|fTm&v!>eo^rGrNCDB<^4ZNcMjUhGr) z+w)At79o9wF;5WtL6Hi zziCDE|Cpe0&y4wRK8|G`*?zpdz#GX*_c$3maLsvkIh&VzZLa0D7KYOUH&)67Tjx7g z@K_F!f84wC`y#v8Csc%{26WfE^FV>;;NMO!0FjT)HP{uipRK4PtQzJ z-CY--7La07z+io%PG5#szHRd3r{Me*8G6?J`sL$uhwo7rmHwV=PJIXpQ0vO(|CM=p z6pda)clFqLNX?5W+BC$88mz&jPc4*WQHz>QGXp*+Nw#W3Tyq)Pa6c!F?ctH?-kVci zj<#)-dX*to6@4WaL>O5U&V~cMzv$W~f<}yU_Yfz0Po{C>KA;V zT%_&iAaMQe)~!KGa|>-Apo6HA*}p4x*yw`QW+w*L94PgFrts|?@cFKPWmyBM?Bfq~ z$-W3P6N)0v=y4@+4kRE(6i0e7jKmwo+j-qRf%s({L^v=DPuo9%ha+CW4+wxuUgJ`# zR8Ev5YMU%kR>uIE6m}FHclp}>Wd%Qv1e{-z9BD@RQFDEYR?_u_9}C@E5YH_PbrI@jvpSLEu`A+vYy&+JYWfB70cf!Q7x}S(_hec_LJlX9C<}&A%pMa zG(3>&l+`Bm1NTU884gI9P4C#nRSH00!>gzYoBf;@_jdBDa=|!`-ltk0X0E`WSCqGJ^`LsHX0E z|4!&Dxvx%@sOmd=;w{*iY(4@p_$x|2!=Z6$#!oR zMeMoE?*A-1PT_WE{L*>aHl{ApVETl>8`fX`G z+L43t^k(nWdFN*%068tXun2~|1_B4 z(3^&EAv^+n<@?(pl?Ha;C-oT@o0%pIY*qgGvApU7c1AzR3tVeVdWNgwlHi>G{Hhi6 z&nVJmdlBukhNpKMi(aeSa9HXTUNwAoW!a{MJZZ_)As&-|MO)L;MU6i`_R6XuYY^;p zs*n{Uum`)-nDc=>rE5I;{uq9e5(ZBV8_pX3JY)CzfSchb+cx$S80wI`>)$#E^oe(2;M? zs&G{4C8(201^w=#HIi+X8>;KuuFmSHr$~nwJ}1&Q?)UU=OpeWVarjQQ97B$9!ry_^ z`XHIj}a2mJ&)wJIiKd|qHwS#`!tyr4z{F3{5@MYM`ynzPkYnht+lAooo;Cd0~ zy&;%tbgt>6ms&LH=MHu0KY7M5b-G#Uz6^0kz(SalL~Ju8Yd{-}e~7{Ug+qs=>+{#l z9jDe=N&D^zSoGDIqQ@)1ytaN)HmH$S?xjeZL33d#SizOOmAv=x1e?CN?(IPOOJV3Y zMSK_wQ~Ke-2B*c{FEQ_?-E9apCbC?1PT#JR`Nxl7bMudHnbuydXjz?(%Q~NEqR`P{ z+m5Ze%q0f7WM70q(D44bnTcIsdTz<&@6Ao(5<%>#*G}RK4EL0Uss5#<1rW1E3?TO> zqm18M-`Q9{!6L)cQ&HE_)_(=;vaI5Bu!jUBukY!Mn`#%{|0!dtFZwR<;cA2j-QkRV z<3>=#bN8;Yri$Wg&u<4-D4>%^eR0#$B2s=(`~)X#I}Q(kNd3Pp*t%wUi)JnP98SYg z0p2y}!f-Jq-_Ijd<9Q#c=is1WNYN{()2SBBk4v-vG)*J4+<_SE|s#TN)HKxKQnr)V11Q83)&k@Bp3h)+}&NDE^f`*JR!igQ8{-S|`stdRNn+eA(p9|!#ngsFM z`qb6oF0kG;1LX<0DRwx?_))^J*yaiD ztH5vK4au@Ka}!f{pJsTFub$mFyVi`H29N9i%5q#q3CQds5c`nso;)If9fTv^hC{nT zt!&NJ`i89d@?8g)h&n=Z0~1d-J+(@?CtNZh+112qL92Ehf)p%Y|hs& zG1KGDF$*?6c2q!kzF7jb&pTvY%U$LxdYsCKAK}oQcrI5Hu1Fp_ikqs%m?ZuK@LE4J zci!5iNHd;n-C!0%^L{B7zOtJhs(*_^{Zxe5xe}%*a!YrN%W3oh7~a`2PmCy+SmrIk z+HZ@=p3Sj74ka2IVt`xQse&N|O8^c`x3eQUR_1^D59Q4_lD2 zRLK%W1%zUmb_VmueLWRET)yLVDIN`0(R>LsF|X*68NE$YZ)CO7(LV0AdJ^k>%m=#r z0&HbJJg6<^WD%Jzsd}Nok_d>yOU7Ok2JhI0laFyG-h^{13&_7e*xK^jOsKa;#^)y8 zE@{Y!US&n=d9Ue1fFSV7tV}a)@F1OF>OJDiyRLTgzWJLsAl1sTYp3srakOZSv6hA5 zz?jJ~GPdMArNM8`>~(xEcweGO%0ub)yFj5%YB}9W1^Go3blV3uItYqoXomIM{4Sba zIv0C-wmj~3@PHVD$1tpqVdZwLMY#@i{b15Z2bAAD*8GvqsypKX@ZWIEFHPAlK4_G% z_;7WPWyluTJYGowmh=VZgm-VEhs+)6}#DKSBJoSL;5X#=UtP5}S23)wwFqVzZle2mUM^oD^pL zChPgqfnLdvLe3|XE54zfORb>xd-dY!ZY0?QItzD7n53RdYUGC$pgOURZd+Dw{!2{r`g2`Y3nSk*u2*XRAE0$;enE9 z)o|@O5VLgo((mH*pN!4nMt%mrq3LRr(&{$7W!<;DCdh!kqsQhwCMaJu8*iIg zZ_n!jtEufN00%3o>S#P~jv)!%xkQ$YthJnj)%1u?vYM(F5(YCPb6E|Zcx-xRTGrC^ zdR~Elw0tTBc6eljTqw9va|0a3q+pe+TcpC`|CmMcycD^4b4Ah)V){ck5i%Tj`Yk?H zl|Lw{oq|-#$}8-7AE1uJP&_8@M`#WPK57V=+cUdSzt8IjD@HhyF>eG|O~6C1I6>l+C7{9{;ve`C{rasiLUfK98JG&aE2^;ONlgA4975hCjR8Q?ceczWW zQ&m^iTDD(v0B!o7I6#(!fXiq$YnZr5<#O@hI_4>@bZTGd;pXDh?Wu6aZU5dTRCB2( zbGq*aIa;=8q1oM>J;dnB?IBsmhQCROGNETr=^i&GhtGd!qcH^!GMA(fM{2@K*tL40 zBdI_dFN}%~18H7StZ+?bwRTsiKCy7kR9BskQEu0Vl4~mI3UTX~fJ0dou|Fa&Xd~t3 zyN|uvLvoy4ZubIUpQ{6}-_mF$hjoYBOgEh={43x_!Oj>6Z0OI}#6QMzX%#3@r*JYp z8*bzWl=knWBT$lLDGXl_Mq8IB2wIB|gE08w)Z|Lc>EZE9fNerKP0PKvS2RN#9r^XT z;|Bp|i@U9JD|hyij|xD*|37e!#f1q7-P(X+;(4v9_nW-gnNHpgroR;L{iZCrTHQ}8 zRIT~4W|#dzB0g>>(TqS`jp{~GC|*sD+HkX1`@IdNtqD@>q|p}n1Wv%Yj22f-^i?A) zESmtar=Su|x;T*N9(jTJ6VnIL28WI(Gw%kRq7eLh@PU6sS< z=i;N=0M^G1LnBLRMcZnt{;k>4h&(%eRPz*Aszen3aU`vll$g1qv6V>@c8`lt% zpnT!!TdOU6L!0uEO+`=VOigSrw-Q_xBYbiCCz- za{zIw(Ew|Wfv&9U)9Fhdb+k<5zmanQp^2b znX8^dE(@qw3jl=-!SA-udqc5P)S0s&2>&F0Bm(I{o4i+rcMQnJ?Jk5si z=U;U2XOZ^f^7(4z7pg@|R+T^LjMrWqATih6@JT6P6^uFimtL)+6j(BRu*Fb`cV2-a zRq8o>?v6cw(qehQIn;Dz`_0vv_20KMtjjg#ruEyrHn)pj?~Y~?l0XQ6pS_}coOcZa zuY&uEXugfAl=92y>4miFED?Zh?rhBU{53(;eO;V zYl#XR$&X)cm#03zmMgl16G*wb)?!h3dndT=;}|!^oA9?KV=N3cQBlYlf+i7VOU2o2-M>x$Mn>)` zW;ezSt^evnUA`FVxrwg>xAWxT9FH>orvLL$nD|eNoJyUhjpJkfrxGRlW}n3YR<3v#}QS@^n>gXO$R(?N~+d(>&%=YDVPoUH)N3s zTRq)d^dpsQ`$!Jg4&m{Q22z4I=AVbi*z6E9MhJ<>n*BEM?%n*Co;X}DGHQ#VAR+L) z04owE*KKxl)sT!4cm>KxIu^m3QLFW~VrA9FeuxH>70&`GNp|@MT!<0MHe_m2Nf%-N zJ!P~&-05AR#iIu^phR1Kq7+b2Ez%8CeKF5I#VCT+WERsDfGflfK-KyN@5AJy&5jty)v^7~o!f=~#U&VT_((OW!t;Z#I}j zJrx*DwDNf(uQ>(U#FP{EPDSW+LLf^=Lm`b+ULIeN&4?4I^uCUsVMvLW0r^Ygw%Oaq zxn=g&uL=dz0L^P1gT<#bX0U zw!9{Sy{FSSLVIgUOSzMmSp>wkYZomaC!*0ojs*NeV>CanMEiV)P@jrA#r5ee_OdYM zFBR(qjKeRPQK6f>@+eZbbXmJt7XoWH#?Y@{2^V^`TZ=*i1tzmOxKetp;@`xMPIusf zM_>#H>^VC=LHN`kyHT5Fibcmeer@f<2mBdmgKpQ_+?q2O1&~)^eZ~gHB_krp<_N$Q z@HMq`$Sa`%sO>Zgs8(ICK`Zl zXRf(>aYtm%-TLn}YK(Bij2eGZpT_jyYWaTZ^@C zwMQNdbJIol-fZJZw|D$6ycXn``x-}M=P^;-s^)LL|0cipPdz10A(YN>nM9BruWZ9=6p^Azr-;2fH8WP0<^Jk| z6SL_Q{7v-zbvDX_B@RyE&JG0?rjxC)!w(zELB)Oc_R@ueNFCf(Y+p4jRV6AkClSwC zY4^ItO>S^Uv;Gaf{lsy)14)$LHf0}}SmDn1@A`d)X^g>qKWv`2-55toIr%$p1RDoz zrf$`x8)L$n3tIGS`mUp^j{z{=R~MHGV5{LpSQp=-JURXIYoyLkoH@9!3g)x90Mge~5^xbDZi3sR z(bG}u6Y(resRP@!Xox0yf#iaRE4v2I_WKWM)uW*7fc$G}n&xs004-oAo;k*Fh?@c7 zS%>!YF`wcsbO8#;OTYu&4hzUCY_b}{+zlYBS6Ht0G)x9UwQCA6WyDe62~heb$W}4a zm&FZkxe$)@E%dnSen$QuT7Zd_VS^hq#NKR!=QYOk_AlkRhV*SAVjnIS}u zIVkV`Y;S=AFHN@~WMenVPiQgRpH!dprHc?;B?b{Hb-PjON|OM$ZtT0HsR_STO3J=HM# zR`*2>ff#7T=@mK(MjDq{X6scL433LV${_%$$t2o?X;6_3?rE%_4# z>DrfwZ7OvU$|N#D$ELQX9_Uas)a=mZZkqoCs1kT}&L)P0#%hv^01`|wM&I#4ksB0i z3<>EDl)A@dFvb3=%_{)u3Ytb5iN459>0ijlH5}#Rdmmb)CAUQ9(D$y17>0__z9WRh zzp5e|ld(Ypa;<_t>XeE*1r;c2kB9{;jF3;Y)w_cQ2B+mKoWfHp>&PS4>5;{&46ByQ<;*7s9c394eBX z^@Cw^HWuBFo~H>_Ilhlw;4mXWMrICt)MQe;uP!N9g{4Yi)QT};M(qg1u2^Uocoo0( zIx1WBJuv`DFPOgyLX@}~7CvKvv;`}-E%6btVRtlvtAySke!!))n*M7;)peo|O~OEh zzWz8R*qp;jZ#LQxB(jsyb55=0enBK*?0l)NtIp!Se}1Y*0)PCzx8X_no3*#WgWaYrO~$B zSaX;i00hftb5jVpg)Mwzg9BM9zAsR-SdtGmN574DV_p+VU_=Zm7_?+(T(5P^43Rz? zfK7=V?uGne(`N|H9^5$$-m@--|x}t{%bA=gd*8?vml~S7Q zF6dtTJ0&rjjW}zW_9!K9i!lA07azUR5L{Gty$i1=qxFX{dM2?HCPr)04Lx zHn+>(P@LY{*cHE-C=%TNsc_4Rcka6V4hB3h$DQ+O36_69k|Wr9+YtYcuGUc?B(d#R zrs&XJuA%~zcGxH4=(2fpE4K6F{0;HnSrW7Dj$_5%v7G)rNes*Pxk)B{`_+Xd1nu!Y z$8TyM77O9>pQZNZclLV$GE4i^t)iaKB98hZMvhwVruxnM3)#$BI%Z#W>~7%i%ZXcm zNa^ohqidyp5=h~AM-aS`E-lqnaz$5K^%o$oZBtFusQzRzizoHrwplP#^y51J=Zk}1 z<;8$hX;vs&TDrDv2+R*`eV{2=@{%?PG@4I5F93wcY3Z4(@C9lihN;?Bty%GMCGK2x z6>8f_T%Iq3N1DYYU_2Vs5@jkE9{xy_Z1QEQT!Ln-gca5YNI@?;e|TJ7>bSc-c%0qW zaC2_wN8>zfh zF>=)P-Y+8*FeZs^})O0^o_ILzjnG1e_jFvt-dw&ItHueR_`QsIev{ z14ZNPs3(6fjViL8-?xuidczZcyORZK>)a0qyCZl~&^AejqFA#kmVY01=≥<_h!+cuP>cHq{Ok};xHunzJ zj-FKK8bc+I(pJ8$+BB@PKQK-DBe@i@DXiOF^%Db37JCU3~ zgz9Ol7xZPaH7|i-)#q2ev@fu7`c0MhnZDlqeP9spNXKVi-$Ce6f(Ye=$6V}~ZgOaf znPQfetMPVsRHLlrBnM_8RI9DX)i4nsf5@Fe7$>+yEQnXgcWXm>vA->Cs$%5W zO4n_~iZ44z|C{<)Sr0{V4&vK7mtoM$yG3N5ny7q9HvlB(ex)s7j7-3CG_{h z2}`x*cZ0P%~ zFGTVHz3`7eB>T{fzPGtB;EQ-sH0qbI?X`dYPrXUYSnYjg}WFXv@k_%E1*TX6|=X za=~pH(EB>D<90n7gbAi8gXPm$98z!}h+I<{FD3QN<@L{!QwZ&3WZfs&n~htl)`{cd zygAr^N<5s85#41i1Veuq0-1@&W)0U;GjlgPS!*wT6I>E& z8~qL=nR@7iRRhL~j~4B>-&-jBI;6e*4^mhlGs{<5-*z&2IUU+(Mp-w#3Zga!7Z8C=^;ezXN|7Iy)wJeX*mRDCH0d=ZwTpw(Aw(9Jdv${zAi2qd|pd1=BukDY*KRB{n^%Vg< z+@M|e=Y~&##@(!nJiA;qOcV(+IBJbL?nixaFuq5kAw0x8Z}&rduw84G76@31As{|; z&zcZ*{I~>%PK?y1fDBN(?bevvsV_r;NIz{jbRhRE6At)= zRRs+7EZ0xHVE^`=m$&NNtan?h!#pEtGtI2T=q}EMCSvGm;~W%h?#_cbJVj5v zb9d23Fz8DciF`%rd8R7wnXc_6d)dxv+lOHiH1oGW$FBLWF)Cur#YQ`;1Q!bYn_lEp zy8bUjJ(iq=Xe|Oi;T&`g{3I=MN$A`G6hj^&QhYc-x5r9c)nkn^G}8N2=JgELH_99p z_fTzf!vjIRW274o=!sB(k$tt918V$w-Az%N zJ40s~{6X6fk?n*Yww0O72qs3JBpgz)cK(PBzIhjTMS+CMXENF78_pwL5j z`c;D9UI~&z6Yms<9e(+%kk4PH;|jVba+xf47jp&p<)go9Q&6A|38r;(=v!kZaf|dd*$DL~7?26yWkSeQn%Ez)+J`GKryx$}$h|j5>MpxW-)M!OU??)U3goB&0c%F3*_j$s7fKe*396^-iM8ylm`6!f9)1(9 z!}BQ-AJME%y%xu}ikX@b@?q8RFg#d*?(_W2-1C#O)$>;DvXHjOMnq|6K4_a=L#6^3|oCHIv8B$rWm8$+x6XnTq|tNuazf(12g3QUK+> zH8ljbP67XBjeLK^+Tfp`1(OARJC^%juv$olib;)5@#Z!OX5+O}lig>DT`mVH?tE*| z#r91>_?vw^M&<~97!M?0m?Hqr*b`)Lf=^Z$mHwrq#~c<3Iu$0&mQqX}py;dS`ot;( z3Q~95yPa_T8Keuv1_ym%RN_87bG0O1NG=BXB6=Ecr^{%}pw328Hf@dl&&4L@#t1g8 z9|6drQ#=tLIkRG~HObW}Ap~%FG`$Ei`Fd8}1cp;fNcZmXnA(W|(>T{;IdXF)RTac5 z$r6}fldpXe5)gDXD+OG_Gcy^#g!h^mPgc~nb&e`x%)d$S_Wy|}Uje)Yj7;%KachgJ zbAmFp=XBe2`T0f$r+lmEe}6{qX5@O6Z(D2OoX!bE`XxI&u_1sJ`-u>1Xx|nwRKf@H zHC3Yz1y711_Gch+1kMCM%d=HUI5rRaaaV5%ILGtiBLrMrUV+mgufLvAw?!j^2$`u! zX|ek+Yr;xv!oK3+J(Vrvu0E?CsN=Iq7O=}v8%BRe05sCei}S&;h>wVPT&4oP0!gZJ ztR7PbL2s{3w-}gM_|CMv&#~`kU^9;r4Y{$6pF$p%cy9iP{{ImvSezVDO#PS}tTw$y zk^^k?>@W?~1a76&rOpP&YiTLSIvn2=S}E;92rOTJfBU0vN0Mlb!9Kt`w&GQF_S!H8 zVvrS;+0)$u5Gc9hp^-sn8($HVlgv@$7 z=mtc9{_=V~XxPQb!(-vQyd9n#pg;ri6DRfTqbhJP#K`gy{33ntQ!Px4);W>W+k8xuzw?Ni#^X;ZX7-$c)`#`6yY6<-K#1*U$Lm zCABW&P9cwBOD}B4$rgTLk-~*=oVS`|&ajF|#fzm8Sg+MuN6)ecQaxM;WEoyS{_7x&~v`xSMOuJNlB3S~qS;`}$^k}*M z9CX458CVRU)Y(=iM{Z=hN;(!{RE8?X{b~1J3#GYSaO3DCXq%D3%GBuu?l9d3zk)6S zW3`BOq!3+&R>hq^KZ6SE=as?yk%p%JjKF$gV1&JqqnkK}r7*mp5{XktV;lqSMyJEB zlf@q>u3)tDPgv)ljPdkP`=>Wsd9wQY27&mS z%S~5I0&e3yHQ7|*rZ6K?_3;>F2)mV$6fXa--zyWR&36@^M)F&Wv}nmI`FL~V#~*7# zB<%C@FPKdCbWb1{IANcnBjK;m~x%;1tAh4mMdo!g|K*3J{26!I=A>q>uvA~MA7Qm?+w54KZ)yiCGnujPDQ zBM$Arlf}Jjw?_e-g-H0MW1_w&7KqQ_DildD>TDBy%~~m=`sk3}@an%;%!ryztyU4){#eYLdbZSR!B^Q$olDLg+l z2H$z^>ygb>cwVMWIPg)V<=>x|O>z|mt-UULo>I>eB*^gTFZW6qM4#R%QpR*GsyNfk z6Gr%2 zW9%JcLLP<6-<=~v(}}O7_?Q;PFIxST%F#A#QkwyRd>P~l$=TIW z2rdroSdUX}hA+9F8GS|8Q7&3Tv8ZC}J1ejA@AP3ff*2L^NW@NP2L@N(9{DV@o7e-j zs@VSM{qTI~*+mnAAOc!)QGo>uM`3%-8En1mhCVoh4*ff;$8LDH`VT;lgwEkr%?{cw zp>8kHFa8TWAET5s$^z@OTnXG!83)40DA+mdj6q;7z8lWwCZ0@XO@;Bj&gYQ~vv7tT z|BU0=vcGX$lZHP~0+72TpQbdD4VLAfUd>J>({;Zm`&lBqZg}3HXU4sZuDcz|k*ce4 zVEYtK7vYCgBsSK31k;Z*TAG^P?8Teu@tN3DRhbgO1_>Q1Y*}{6gHU!NaGIkJ60&^@ z8UienmTu(_p0VGqEUeu`U60-v_zu~Ux9Lam&ztw9Q|DG|%(M@lJu*F3&eP0Q=``D2 zMWPTw{JLmQ-7iXH%h;N=?zDSbyWO1bEEgNBW|wA%wdoV3K0g-kya{;c;}WuSITchf zE7V!MUkQ1j2To7_Aq(fKa-ac$Qrs%UTj}?g@ehgxQmzLd>d~N^hbIVrsSX*fi191W z(d%ABs;)gw%6bhORK_7J{zP4Y0i4!q&K?ujS6*k?!9L{4Q76&65V)zx@ZZCY*_{;| zaJ#r=^^T*`O$Lup6?y1@Qn1;kLs3f!Auxny2kRB2mJThw!F(%=5>p`s;eDNOM{d@u zf3q`%77v#wD-^XbIhCJd_7vK>epk2Rwk&D@O>-9?kJWm+e#(+Bqe8v=?tGhGcQ=;F z@9Nc4;qzUaQ9zex(&6cNPJP~_0IR4$$WdHeqZ8j!sOd)U$7y%J?wL)BBAKmhqNPTZ z0>;`_=f(3Lqh5eUV%0+lmr0vSOn!(Bvo>L9QrT!`+35Qs5~H&PruOab(-VTnEB0u- zE~_defY{Blo8q&wvyv>%(qo2Jz*uO~q=`=$m^rHdO$GP9-2)5wZkIu!m=HxhYd8cN zK8m%4SK8!Q>u%SXZZmCtg3-&-guhkEG6QzDOY|nOuV>);@r|2l$|f|tnVg?uk`$?7 zb$6*WdI17U*YLJ2M7iyGf%Wg79awCk-_Rz*o%ohqYF;n`^!*jb!>yB0Xv&eG9e!K7(3bjbg8#y}-kF}lFum7ds{nkbMoy=g;I@5Dtn;bXb zlTJ!eYN@pIWupncOuZVJfOt4tC#5{je3M9Cvm7J8yA1TdE2JRS&KeUcqEXh=#Ivin@#M5S>=nu^;~u{# zO&s2Hy=DN?mDCBu1Ef9{dH~}FLXiBMx%1Uzpr26ZfsQ>KlBj*};Zi&QGi&D;&*uU! zHrBd5<3M#Qv%(cZhKQf;gcYSVh2YQn99&xoB#4>_W5U6PQtZabP~+Gf!y)%Vzd-Df zMEs8DgN6WLacB>R;*j;z3ed#Jv;@1AX?v@9MFC>NO)^|$QiuqMwOZpU-rm70{#57HCR-Fmr_D%9f{g&{ThUs(J9n&F zvK^Nd#T(j7GcXG6t@v%s{ysuADR8OFEXH-32vxZomcoPta z%39;8zSk|_zgO(+EmHJk+kTQCAafMvHY43#}-S>i~lM~3x19y z`o!9?smZ+qEBoK|Tusa1INxvVh^{;dOlAPr#^>aFBD23z|KcuR`@1Zr<}NnbS}SX) z@1UaJU}N#lSAPmgHWjRDs(kAkqK9Rk>;V5zM-_)`pkFVl)0=j~;ZdBQpa%fu6{FDf zFaRL5!$ru4l1!9;DL$8=l~M*LMPqqbB2;KG-8eCxwXQBw!`nCiP<8#q`7{KEGug~z z`Snl78SjTh3l*wNr|%$}&dll&Xz7A&)m1GY|ZwgSC75^z9kbpaPEZX;IVl9bzqO!1}&YrC@I@i zxc+fuv1z1cN0YrQWX8X*vi)TpuXUaObpW+|4Xr|9#r`;8Ex|c{#kA_~480)EA=R?= zN9$!LTkIm+01NF-4AGjcuyv>uP2FO;EdP~nx8{+|i3KzON@t_5@W1R@av-|hmZ1{J zM1FE2+ON~FwT@9sUZ#`f>i2wKi5MOq*|znxdnah6gHFYSQ)qqutHvQRsd~7%uItiV zud=J`iGZ@JGo!H<8{K7^8oJ; z_tLb|i&lOb=vp9{ZOtrpj3Rd>CI=WTS2biD@ohIv;uht#sHKnpBM z`q-N~zqf(yIJ*@68!Q(8X}*ALCRh_jdJ#BAln4k)F#w9^Qztr_NBW9#bDk}ZdL|zwXR@>xnQ-Y`%x0O!h~*>KnKu~IEsQp60KBUi z0kyveP4!22UW+<`KNmdwC5dR%jb`q>JkIRtPM+y-v{J@whWju*h)@=~@#g7%;^bQo zd-MChkFcG?P(;-aPU1J1=qw?NECLhv{5x0gnV6v_-Na)n*DQL@>j|DFeYP#L1(wA;8Vb@r#y6q&A91(%#6AQ5*X>ra2hm+0 zH;;}2eY=teS~m~jx#^t*3D(=jo%EZ)e?@t#zw9rZSJaKj6W5uE_(1F+;A=vl0UVk- z?>7S%VNFy_iEer8Q+&n_{lBpYeS3z(PuN&^X^dz#0H9fF03<@wL4;7KtxHlVe;i`@ zWtEw!#W6GH>=qN2O-?fa7h)yKN|+d%8Ow=)9im8$RX8NNDI|NW856hQ z<*}s~(UrC${NDPXb=CIQkqL|Wy&-GQCpW~DySlWu^=-{2-a8@XqxX?UI@xH}Y<`g| zBm2D%JV!5qZaCg7S9udB>YSWkL}(lDU_^dnj;h(4jV1cyG??{<&RbM91Pro#L zE%)AlTi}R*V7hRLYRg57Ufo3SIJapqoU#Gen4$*~UsbV)-ux5#@S|q?pD$V^6AuWe z`3~;7H4FwO^iDOBm>;joU{h5))ht_V_?Z0@LEaAN*8ZBDR_Dm=9&G2iPUkWN_IWZO z2eKdZ1KXmcxP;7t-;5XX~un=Fl*Re(>TqcUJT%6Y! zHL>^P+YVSp*sK04kG!A_FUksbV?(l7SUgqxVQr%);_`h~zJ0+)iopHYO zJqUeCbo?_;0DueEdolkuLXxl&A>g)9$J*(-xq&+>Ll-RWqOTN7630RvBSoP<$CGRi zB_&CPhJBg)n^0&NU~Hq66(vzW7z6LAfrBmT)g{^SX^VUZLASnhjlbi|-UYfy$Ww{2 zNGlri!G2}1JiTF%6qWo)Pyn&-e%&LAhx6vjCj!PO_2LXS#d79DH*0*>Zg(xGhjYsM zX2_kET=Di~hfqiH-?%b5)9NIdT_2t6n7l7%D%2sx5%#AVM+TLW>34)$f&87&c=zBK29>KG znm^yi(5anCAc@X?&}2|#?(|OA>XtU&-@A<+IAtEWn~J}~Q*TFd3uGKxaavjwGqWn= zrLJpUe=kh;tv12|uOTk~gy83H#y@dC;LUvWY#%Hz=HWE099?bo*M7D?po1BPi~2Na z$|H&6znrK1zXn)l_t8@V98i18dov3kpRb@kuN4X>p!4F!kPcw2KmI%MA_gldISDBs znHphiiWR|xl#r0Ee9^NXRwi9`$^q-GZ#D?76qs5QoR)IiZj=HtzJ3BUxPBvv{KSFf zNJz*#{MMR3PiV=eCt?|*J%;J^KJM93IM zxv^c%$xGJ(KxKJ+BvZ&v`c$T>`D6YvG5g1A^NuV3Q;d;>yXY;7mUN=1cwKdSxXyz}hx{HI$XdN-5Z z2P7NDj)Sy4#lHOMfPC<`t2yN;Bhb-x!DAGY_7M%^4-D}^J`OC^e@yt>2yiHoFn|Th z*j`UhSOL>%x#rw@l^Le=wEPI&L=ih79e;Y#M^&!I4#^n1#VGp@?SGx4`a;4?hAmv4 zRj?-dIE+Yob!7ZN&5RQ?93{{#AnTpeQVa`t>jTH|HfbEnP)Iu|sh}}4QWX%~J zixF7MgIL^9IDp@nmX0#|k&ZR^oT>zP5t08W4>rJ2mo$F20GnP=1Q4@ej2Q>Y+m;6j zN-9=bS5tXdk2fns&U@Ri&|e?;JK-`!`wQZHP;DR1*&n*1B|D?-~x6NES5$Kw^+ zez22%24-g2y;|&LZRI`fQ)TjE+F#cR+FgzWEugJz%)cF76@R`d=c+q6D(!b-E%nGX zybI?vi`~95p}2cyl!Se)Q1*Hh9bF@qE*oz}~EO;ffUiHLgIE)Qd%TD9x zRL43c6#1arHAodaP8IwC(Cg5@bhT*nlTsIPQ*;qQuMba!vjx9K9Nkgl{_4U8@A{~k zX!_7`qua(N-UtmvP5-v6b52YiaHDrEBYmx0yas+Iee01mzsaKqva&Td4ZpfZgNs|< zS@v3T>_>kL_)q%;l2{E?l^gUlclVK?8r9?RUp6gxNmgvc%NB+)u`G@LDzO*!HUn9ob|Xl;7<{TAyz?W#0*ncQAz>vy|dD z_9RnX??188pb}Y_{&hs2$b`^nYz2ls*y+iHiJF-cc&O2HkYjbcF;wVA<@#lDjFYU_ZTGf&L z;Y>4%PiMXW2^kiRTsM0Ms-tEWcUr}WLrTY^dUe5oy^b@C4y7MgAs>s!=KN|}x1Ml) z@?$1Vuw*n9vmej5&G?5fKF!d7e>sz+fqFbfaKwQKd_~Gkh-xKs7}}bl{zHfZmx^XK(3Re#3Qw+%etQeY!5U>Qk>tnEpOBB9! z>?4=yDtzd$gbp+*Af=sxl}Lg`>jE7~kRlZlCfPqEyY9A3(I|B_SYGk%s6pICJ)G{e zQQhoQ3-mvm4;jtE@%gT@pe!WhuSBoL^#scy+X%)&kHLu=8W&Wq$%Fx)eHCvwz7OM&C|WR5d$@AJ}V=-Z#O?Y zG;^8c4nOvOgPr-k^f2r7YlThMpw4};)!&x-b=wNI{-Ci zrq6=sjR}|;NPw6q$-iL;^8o-o;^1w`tp`pc-cISr)%~g`z*}TP3u4l)Z$@i%wVEMd z5FPrGddSVIW#Je}ZG@gzznqz!;JmbCVm|Yr___O%&cApD^DqwtiTdy9CL;7#T8v7U zBJ=H13?U6nl=sm~?E2+e1`jG#KYymV4UZ29aFwJ4eVmQj35l`>H3RKvMFu;=`tqlh zr+vHD^}G+*z`%@8my5W#3xZ|25EcV~)Bvdj7^Q7ryI`ei(;p|NnmKWh>Ok>1>RLwQ zV5bT*K?j%0c?)+CUhOB#Ew*!oS{Xgfc&4fmq5~hNzN|ygpBI1duJTLEBE-p-qvV8e zpMy}y1TBgq;G5PL+N1ad4?-Xcl?jU02T6Q}CA&Yn{bxXe-Y1I9`nALS?kg zD|1m8_O-gkr9Hz%GK4HGZYn1L{%N&V*O}vp4jI1|Lt={J;3+N(O{@`;8XE|~BEzN= zLwTZB3>;qBWIfc%cPvofXRYI6L@HsJKldoXVvOp8SD_qfq4sQxHZMc2Bb-csPCcA% zSG8!TXR_8-S&W%6;7f%97HK^fl%At+cj}aToxE&nPNkYHB_vMXM1|nL(cB)m=IU;aVR?h7BV=2 z{MzL0beDbhviJ_=6|tg?6b)W6h6G<#1OkOjN}ec$GtDX(XO&A-6l;;i_ve7u@*)Bq zV)uCR(o-!9(ghR+peGsl^2<}fE4tf2HbNHf=+75FFoj`Ee7lm?!ipvd#{2apUM?6H ztd2xAezRl`8pY`HK6dy&85azvr{qrldKw=L!AM-bo_}{E<^49eecM-pQPKW-IZoJ$ z+RI@e;gq^Py-%Bl#muSO=x!g)dgi9ggR9qO#rZmaWtmPd8Kd^7w0Ik;NZGH!JUQHW zgt8@A!G7LXj&*g`X!d}U!uU~g0bfNZfc49E^C6BPIIMAL7E?q!U8Zd%nOKsS&mEVp zg>0Lu=XqA4q^p{t?Bt8-& zo#Xak`BUuElZ1!w;4X%X3z|ZbSnuecL3Q&jmKON+HZ@d$@IM|sa*nUcAsjV+Kgjms z$p0WIKRfi~;QebmUJx~*y6dFOJmvnKCj1w|`{vOk zF&-3Oz*};7YHxj^fcl68?N#-NqQv|WmvimEO#f~DIIr!>ASCrJ7kh3fsdb3*RlKO_ zIzaUALre+2CuaQq}dfO9aIJ(AH6uHBVXq@efI* zsA%loFjJ)Nh10R@7mMmy729GqoLq%i^dDP7fsv}^+HLkic`<9>Fr7|T(dU)64)I+;I`H|Dxz z0@76Je6iFx^^Xd|AlU+8fRwMv6eW~GpO7XTbNY*&G;ggUyZQMU7=}}B9y5-&&pF*z+y*6l*8W^W+oX|*MhM%g%%`1BHBatM55{@ZO4(iZXA18EH zCo0vfntQ0^*Xthv6;}yEhCkq$B!OolI(v%|`3c~6Om83iR=g4)m>V+YX3>CP1pL#(p!S35^V)UfqFs%4R@oPcB$Qs`mTKC7V zZRYNETJbEk8I;eKmNk$8(5Le=dH8C4)U02r@X7MMn{IaZ+sdH`7WytdF896p$2M+i zN4!R~!t-jX@!CjF8guxt&{7kx^l3n~v}F@L%g^L!Y@WBta$WldRNS;GRQ@7K1ov|u zEeN;OpNg^97@04-WU68pe$bspw$2tl!z9jMeTF3lqNr=LBe)QynK%%;ZIQYe;Z`j= z88f1yieNyGIK20hDj@s7I}!oJry~&_sLH*ytGKE0W=fk4+hujy&;dml3S9xr&K%P>O!_u;B&B~hZm0|N6#7MADkN6|!pfTCo7YrQ43 zH?AV3*WX)~0DaMH10pc6;v)G)>$8&SCb#5l_PE%)tpkCbXNumVW6}gii~r7bj1d9A z8MCX-Md|s+=PO(YOI~eRQ0$f zY#bl|vK?QKyt-!<>oA{T4MLQfmCiCf70gPvqXb#jRnwHFXqTYBVbJB@zK2Dx-en!V z3AZ}X+Dnx6Xe-%-9%W=fza~trE0_DWGk|M(-m@mn`h^c&b@Q z<4YQ%)wt>}5jP$FpDfPw>nZFkwzu8A{q^iK={UrfSoHmCly}?_&vs*Z4+{|dpNK^~ zg?eJB{ZgD=0w~@pduhG@Y;c7PQyO?e%zejBRnKOaVIOU^DhA;lg_)3EdtMz~NllRJ zC1%q99bn-9$V;?;N65qFw5Nn3=-iS!d$w`y{I3cS{Cg`JD@rhp+Y(hdjCXU{jckKa z7z449*61NP)Z#$^L~P9>rL~>3x01f@<7#A1 zeQZZt(%&_#;UlN$2~|E#MpQ=D^{h6YR`^-aOF*Q0!uP8b*@*W8K$u#6eY1%#Vz7jM zq$KE0@*}H@Tnd0B+r5W@BEw9if$%7Roi;em4W^p1P9Vi4n!J)-=tB{tU;~;%%3;yS8+W!evEeb2A#ts~Xm) zrQd4rc)yQq(mES(%98B|KR7_w|LN{xlnSY49Fc@^7i{l7cX(=*Y45jL+baXXP;)Ho z@gP~Mgo51xbbE{?A?4zj_4X?!pYKCgBKD4+jqB&A7Fw-6#!h>?hJ_^BsYu{eV+`Mu zI~fQro-r&_K}QA9f(Q1mpP?X71R0S)CU|G+Jmy<=11y@lA!8IVQnih5)JC4f)_8Ke zs#ve(88qP1HNm`nN49~}_Z0UrLdsViH#7^KxN)5V(xgCg7Fie+^>2cplNapcCX95u z?u*YlpS>J{t{vJYhk5DT+ahT3b5{OQu$d#<{#VUXJQyCN$rKT;omUu5qP}!buI??o z)k_2}a=YkXWj9k)!mY@{+}A3%yWI)YR^kH2rgmN&E{9T|bcGGWC^KJwUg36hiQQJZuD;l47{jHQKNx2=XEF+aylhVYngt8% zGV-XSaN@AR7j3r8lEHE(oDXUVU?jv(w1YN?ApVgx3%C0Z2Na8@QlJRIFq?W{j42`& zbB6QrcwI8eI(Ol*QgIFGK8y(>D8Cqe9as#l~PbatZ6vViOw%De#B+F?3G?@lff))hZxrP6h~){H)=fLkZE($Q%{` zr=(#Dt6Fc>QKrs3nmDSAlKG>HU?vx{WZeH}64?qaJ)B)5&PdSSrStlmX<+#x$b{iX*d}Vk?8knZ&Qw8LkBJ|rZ1&&j4{Ny4G48!Bg zd|##G)?9KsD~!%PSCxICs!9YIEi$PbHjL(lg!FNUcC(v>zKMj`%m?+D_qyI-=Qd7GQx6G?LG|05Ij_@d zW%sJ`o4I|*jCECWHBGKU$XScDZv)L3aA=s%ZM-f~z2qZ7q z|0^}Hq)LcQej2)NKU|La|95}fDn>a0vP=F?vpO)1b zpuW1wBc0AW0@B>XvSPN74|Gr;ID}k8@k)J>Z5G7Xi$8^ixnY%9o`(1f{qfE=%hSSw zgI(fkno;08!uWgp5AShp>Me$|fBs82Z_Nt1+86K?Lx-f3D zlD>y2;E|AqBEbTBYAhUay#7i;sJO7u5QxCXsxMNoe2dDmlQ$NYIt!}@z0ni^u+4_j z@J+EC!rOIOZ`wx9qugEpU5gNoc?@IEykJD>CE!hi`}X&ZCe@nLGI`aD`${*Tm5MNsq^gyK{nX*Sd|PryM6X+? zr=nr!M=6pGCIEc@@Jn{j7jhp`rg8U41O za3!}0o6TVmGe6RPgYYT!uhHB!iKJ1n>3oz3SWE;JQWGs!o(YBCi-$>@bc_wzT@-DZ!%=4G%P7DWr7Q?f2Jb zkJQz{{o5WcuiG9dgvXB*`p60(V^p&x4s6_iXei`5VPxz`;)wwLqHk?U=bXc2w)tNN z7s$3X0uUNDXD*P9kQ_EPr`0jNZF)gG|4OBufz?VM$>z4p2AImO!KMHGJS!WljMIro zHi}V0gu#)zzu$k#Z*o!<{aJm-T#0nRlsUPKq3y1G&|vr4^(`sW`GK&EA|@Fh9EyZT z{~YpVJENYmgJ}g9AF#=9c4?w$UbYy7snBHg!@kUEx&|1J6(XiKNgEMh%)W69(86#4 z|8*`Y=QoOacS|$@0(>&61*BpX+HP&?toKdi*A;k&X>%5A=3tqELdYpT%_{hJzD$jE zziH}I>sG@iLKO1c7*}Zd+LFuUl)Clt?+2b%O_#4Ek{L+_6MHlOP{m4J|-O+vMFXUhOeaX46eu_T;JcjZ2E`7GW&6<*9nQxpXjWnQX98Rrc;E z&gA3ibEwyQjd%BwPSL*$d|`0yz%4^9D?1r+&`k5cJrmd*92?D4Yy#VV5mG(mlC9-m zr==^)64tEw0kD&PB96>6efn;qT`p9zB^fXs*tVd6Y^86}C*z$vTyN};P=rJRDvyIZ z#z1qh4be{mj}_kIee#aMj(KWhO3wn!Vjw+tOoG|#9i ziseGhWMv#bGLlH=_o&cfdy38H>}R!Wte%I(L$b?U>%V_1j}1oY(Rri>4yM@t5UOO| zQdc6AI(P1godv7{OBTD1~Rjz!3fNr@G$OK-VE_Uk|9_ALY@a%avhOk zLTSTB`33mgE-Be&z{N9Ee8G5r-gl15mj+h;#*1E&dw#7dS2GRUVzRfC$?&`DJ=r%c z8O57E%KSX6V$p(FCciA?AN*H$rLy{TBawE{$c_0Yn5hsQI*pdytaQLhA^yWDo}d9y z<~TaSgF&>-QJl|tYsuCpPgM~X&>#5+7`Slae#J*c?mcU9(XQe4CyvI5CZ_=yg=3$` z7NIDc=wVM>feqRZ%?r;qKm5>b00DQqfLi>Pgg?SK905jytjKdRusBHNcb@2=$2GPg z-o^~j58c<^zyEL}8Y@K;9rS}If!#=cf3Xj|kl+Ok# zl7*uNG&u^seuQ(xZ6Y_wlKNcPIG70DS~4k9s(>)J#F#J?_70|RUl9BMqNh8U>5u|{ z^L$4;ZELQ1B1PhgSppaTwDlB|(E={JoSzr3dRSKJK&LNA)js4hfre>F)}tg^V^w{- z*xp7GAJjBD*KDQEZ|-_IpiPUD+k~__FP&cXYmg@-pHi%HO5WJC&g;n?Mwn&(z4R-! zmp9pW^gp{jSiphUB3!zu$IyZ|x;u>vTm30s@$*&++9-FRCb;8Ea=QEuHk`O}@XY4* zZ2TPq?g6XhG{9jPd%W4okKPXih&cXrf58oayBK=#J^wK039Tz_)C0g5f|;Z#iKgQ4 zQTrH#hHgp7Dbz2L0APb#UL!o0$CIOqYNIC3{-+wITc_QZ*rQjeFOZb7c&<#V!SGG0 zdiHM0{5)LCyIV4*{rP&BBZ3>hd^dEjRI|~>kDfJJol6@8(F_>{EZq&3{KBmmS&g{r zddJbc7RvYRdB6{&7G1s9Ip^i^lef{~btZ6)bETlzq{sXDLzKs}tl0akOUoK` zEduaeNU-PH>IUSv2cp8?vJU>+9yH=+|Spapi}!p%hcsloQ)1>mk8j&2^8A(*}{pSv)AF5lSARcDc-l= zM6Up#5yKAo-!1h|_`W^n~tb zk>RoTDY#*OdglLzClem>5u;D%k<)y7AZ3s@*%Hh^`)c*}AI$mSMTH*oImRo75P6>c7(?)mRh z#naylZ|t3MCVU7%lj9vOzW~lP2BvHF`NOEDSF;(myr_of#!RCC-8*es`2?ps8a7x z`lgbmwaO+nQh{T|J`DXaEh0%nt|$G&@bO)9a^2YjUQdKqwCGBs?&4lN6*@wH#D5rB zggjnIdU0b%*Cm1O38ME5gsD z#y|lC5C4N*)MZuv$l)fRi*9rCky4B&{1l73qr>TUz~u&>nB|8bcd_#IQ7)3oYnLL7JN@4fS#f}@QE1o(R+Fvq3P{D%FVOW zN^Nbdm$J?DC!9I!9MX8Fo?n!wVEG?)bGjUJ$!wffm#eXIY=&GL z4zuo}cIb7i2+q*pTT|?oy8f+nEO}dYnzMF(8*gTC*Z7yV?;Q30e#+ex5EqS3!h(Yi z3B3YYD3FY=n(B&>Y5h)e_4tzIcL!=hq|rCtG@IQm9*SpG95~?O@(=vSPl78d+#rJv z;NPdIN7=juS}cXiM{u4vp^$i>fnBtuaE9=N zqcZ0Uy-3v^B{_$AS0Fo}RjXS~6+^w^T-RNhD;J)?7AE3z!yF^GK+7*3^>N;a8g)Lo ziJ;BY2|>`{(?9r{j)m^gK$u1R>E%yHV^e;QpR>;>{lF-QbU*?$vYGMXy;O^M%t8bI zWy*g48cu(90_hDK0`{Tlc#Jco&^TL+b;V_iX*UrmE5HI#3AC^rRfeA%etp3N5ovE`c?RuoieWmw>V=Y|cKp;3kCXi@+@s6`i}=cTJ-$7VTqjaf>9 zagugHx%@ejAO!_j$r~)qY>BJm7NPRKo(RkGNq#)R4Bdz9NM|ow21k=3-?k#H-T$7p zEmuT35=Gk%qu*SU#CiJ~uY1Z&35eccRQULH*|E5`P$LS9yqjqS9?7SMdb0+yN$Z?tt^+3h8Lf23UlG^Ww4Qb7mH3T!co^CD94pu z=EHvnPfye3$HOrV&`r&pDwj3>gc&Ktk6Huy37F(3#Yd&{%~j|Vqp;AAgg2i9!yv%* z6zoltk|bE!2e8c9ErR@!n8{`s+H4guOw9{bsfbw6W=FB1KR&FSmKIsc+Ru*kwBV+! z1Zg=5_(?$l%tKpOFcmejNJ^2^I4b(viC&}LP?p@{+LoU+Qsk@H)>nsB3B5B{KD=Hxdpo5mBiKlN zCy93_DUCDQs!u<=E$AonmQwLW8W=xL8WytK9W?lf^rt2B7pZJX4a*qR5db``?pFG? znIheP^^5b;tW20BN~deLrW*gwEiP-QHQozIOC`Jc+iL9HCXrWR2x@lco96PT#R>U>3?kx&_f;A; zeSWbO>OMFqq7ebJ-ND5@|n2q)jny)BFir*+0nqF<0h-AmU>@_wKhN)SMN zgR}Z;pBE7NCIj!6;kW~a_aVELOVyP!Ohdn0$sPsbE+1AG8?kaXmn3rik+}=?+2k)_ z*ixX3WobO=r(^SjC|OB3j-`9!UEBR!tRd;BYjr(-i2HpWN&V8VkRHNs&b#|PZ1+CV zGUlPfbNh4W5W?25>Zi(LSUSHu??#yCSojA-uqn|@^0zzM4dKOM9fw{!!CK=N&STB% z-jn_rk(C9Emj4!VLHq@ur}K?=xhPoqs?Y{yK|I?1esyDy3q2@>r!$`js2Ei8*JQ-NyflHokTd=Fe^hO&ux#6OOfjqyr6FXr;UI z;AZO)9>~4>lio#r#=wFN|F(E|vEJF-=W|L;+=p^rRgj7c)lP5c?cCL9I)pc(Abad? zVQG8xmBmIzVqUYPlIJ=_H(#CIR9U8m_mwAp!9fNW3_irlfJJDQ=cAx0v9z8?+ihOX z+@Rq6`y>Ay!P5I0k$OvHZQq~^E>Y1|2jXpngv<7Ojd)lf>C$p*>nur{823J^@)~g{cwF^^tSULFaYO=U4i<-+xo}e3+HTghuVrX$m3xER-ioRK<(SL0es?l zINmox3ytXv;(2;v-{1}=wTTh0S&>slx83V_-1b?|Hj3Af6ulV#+Z>E$#b_!Z6Kxw&Ng+Zp> zKZ+*l6iPURd6yM|iPT=ksb04o_|(M?qB5^R{m1t}4dnhhQ#VaH-qItRUA_z zK1hd-QG%p>8W~eATybSm*OWpJIU5n6LqkInx!pAv7pFo@Isg|ns$cmUM*9O~Iq<%* zVo1f(r`0<6U#-PpH*8ub`beFYZt8dn^oz`oP7lihH>?E)4)@yvUR^%g26jE5zKUuw?-LdiHfQ0Q7BgaCK6 zjmAnDw*U+v&}a{i#I@B4o#X=rAsF^00%0mx_ChlT%awZwd9Of+j@0;Y!CobYJ7ze; zQRlrq)#r-4i@U0~z?}9n=@BUfHmDFX)l<*6?D~Te7t5cGOXT232p@Z`^UIBQ;46#E z8G??WG5&UX@G)5W%#M zRL!JYaO(YZ4f`Vi5mhfP{1aHsQ14q8`^OAQNIT*xRq%0~0|N=Z7|+1>+WAi~g0KD1 z&%K`9-n^~CFR(m)L3D!s@$kH2zY(=3*!_CR!J8bv z=D)GYi6YPK4evUL=1P|ab2dS+0_BDXthqhr4=4ap%X&#-gEkQ$|I{MkCpv=7&vji+ z=6!iKiLf%P@fNDRCg|t*E?EG0Sey&GAb_9q@-~~!k(CU)5h>w*thsgkL`7D|G|9Ou z*NRe#b7Sq7+2RpivqFg`gZ{bID>q1kOV`8SNrVR9_fjo4^G$qA)a-?=v!M&EZrhdj zlPWcK&22~RYiH4=sZm1ap+L(%1xxhk4(|5OPi>yrgUd&-u<4qVPhDMm$`t{%Ymw9r zB8oFE^rnX>OfEsWo|z_p7!c|=I^ZOvn1u$`IP{%HKS;y5Drnyv|AjG^w_z=RFN&+P z^$GN(ahM?mQK2h&n&0{Bto^p5CylJO$yr}H&hSm#&9iLeg5%X6E}yM=>uZ}|^ljlY zw`P&tZ+5ce_+AHrM}<}+*s%9kM6(Law1-W}7q_oCPPyzfPeEnm*rw(~rcwNRMsXAX zWjo2c(G#-k{6wdO4xRwottwtS?b4*7gjdt~i6}fgX#c6;06yXOt~n3n@nawpq&9yv zfhH!wt%#^g_>H-POgUZ=hV+Lf9#+oQ;3S<_P{77JKnWeN%td|o;*rDU%bh&`{z%jD zdMs4)2##YD#MsIpe(n~hf3P;Z&aHpTXMJmOxu>oZ*zx*Efr2HswSn@&C7)!G6f%CM zD){vGS>Um$Z920F5%9bsJ5AV>^?llk^>w>1wey*)Fw2*s`3;hM;%9vzw>V#q33AIo zl`502{XGW}1i*1$XT}zEN$G`XQFhtX#*p~;KCD3^Ss*9S0?gy}jB zx(ZHK_r+{GqM;8P)2{H%5q+{(%Xgjc?L8~_ONcE%BKm6b)wDw+QTiCxH~w#`#632Mhd9sn1kFoTIQ$Q zZ^J4YJX3Vf7qseQMA|Sn)4@&o)1P_yhjEttUISir>~CdBA!RV8CR0ZWxjy#sP&4qETknNXiOAE zyd>Z|4fLB&EuSRCTkvptN&{#HG3eP8l!Yv0AVH+$am${6^>-egG{Y!hDSkCo z547e`V<>*j%DPoB`(k#z#I}>v%E}Yp_sJcl)_J9f!WDf00GNwmfe`dV;~W7;splNu zrTMXRI3TfiiG+~2=;I*XXbu`6Vt9}>OZWOrqPF*`Njq<&IaIkF8o|*? zrFc`2EqiMJf{)Y~qKN}^iU#72X5`Q8ZnDzGxY`$Z{`GfWd5c!40yNaj@u-%wf(0)FUs0EfTReDm+XXpZDrvBo#14RCs#?FXv!M zyJ~oz@1L*#|eUq>?SA~xJ z4)>qz4o?oaH$QqrN)J1=3t2S|iU5dF3ayXgA2R2dBZ>vIHPm5OebD4EE&ueE@k*i( z{I;6&6YWzdTd48AlATN^E%{l(|K;qk57JD9iM;IbT%Ebt*?qICzB9XrcD~~Ep?x&8 z=}74L&kEsxY61GkX&4tylas?n+%d-SM;{Mj@E@T2^TZZ!t&bGhz%4lus<4oK8rJjt zfsaDG8J$F;<|!ljH4>9m6lpGEw*o+!LJ{9qqkqS?#xMZ=2~RWj4_t3k0oWrL~`Y*ypL}CKKxDD{?+b}DN^=+p20D9^D5Q2 zWCQ_7)2UkpKU^gT!NydeokZ7Mc(-8lbrK&ZI8628IKIyE$V|pUmAAFIbN%8tD_;c( zxKMouQnxd+`#+kO(zoA z+@Yt2A=j1;&6T1$u8Ma-h_DY4CFuo{=g2Z%A7$<#%XH%U!ULDvT=;VIKFc>#!}TkV zyQTZ-JcV|zMMgBLcXWU-{PX=|lz0CM}if>wK&Gde+AQ(vGONjj7(1S27lRJzWAE84#tX0$Kp@ddb;L!c; zV1bjVd|d?L-74SXv!ze(Idjr5zfV3d=`<~WdhkRv7P;`~y4;4OE+552EkoKat5Q=< zZqrvX>o^b_!sJ)R+`L5d-agE}$&b#*z7w<0LUbUv>9hul=3AVd>oJ+Jd@nnUWEw_- zF!4@sUr*(J@`dzMXv#!$M&8t+5a(Zp2o+UfVi|15ApB_23!l)nsQUE=YNjck*K&D_ z*D!3rgj0tBEM!tCnfct3aXC}|v7c~S`yOIhtDc`kk5i?-lf(K%dT$+gcd6T8mN!&i z%->OJ*)(Q*+kd^ui-B+Eg*?4?0pWMbHzHzS;tmjBK5+A)K&x=kEZ*rMc`} zBjJVllst4w=WbP0D+}%_GouwmK-2qI*nt>$gHgwxJV;3vzLWqU9t+58WJ#Wxrk^0e z8bm@Ik9dHQ<9O#1^4XU-1+n&JgpStDZI%4O7cGVqg2@7)eQlVn&3gHCy3YxFBd@7| zs)hUBJH${WaoxiOvbg^MI&hg{x6{jOBnq%WNKavbW3x2k5~;T#m1?Sa3~9ri8$Teo zi#_7n>-i zs*b*ppA7V3B@8-mDZ|{T^Mh9we0%Hx1LBe<=oHu8tEULFbg61xBRIi;BK+t%PnN}V zx*UETYow*lfU9Tc1GwFEH1$Oj7_w%(b|j$I@sEQ^KGK8+l#TVD9v99=)&@6~deCwL zc<3Y;3QxOk@qD>(NHgZR*8kL+`~G3bT3qp72_oM_b&Et6L98|TmEMlU$nC5K2Vml4 zdELinimj6k06ID^FkEYmL=+#B8MA4=8%E|3ic&R&lkeQM1I7xDd(!KSvBrb7{*}EXRhZO< zuSwd{hIonN##(ygZ-PX*D21(?HMI;FKoE0NF3>GIIwq5#{l;d!B@fS06k!2Xd8k=o zOp)VV6`%0O&;#-!jwHtA=6;c&%To-K`0{bz)IX0r z4$LN>^R8SQn=t}%ha0Uu&YLpJdmF=GkDZ}He#+M@Q2IoyvS?;Rv$>d#BnZ|ZE z!m;zcrLhcM!-sPTQpqj#9m#-4)2;SKCg0K@ZH08T5%9w87<; z*+&(&PS(c(G_GxT)o)k@QLx~~O3vV6X0FgU%ZEky-D#f3#Eb0jD!!T6OE3ewOO$2b zt3p&Bwh2DWB^w&qm9{IZFYLlx{^Xky&dm(OEE^N_Q%{{S_J?s8HE!~fEGo17;scjN z73*u<^8`&QN-EX25H_{~D}dD-C-8%PQZ|dX(duNNGlZ>YZkpx{2Q-gk+aATX=qA@6 zQf{|-11DdQKF-etDkCG^TGhyXP*p~M$-WThHn5(4+%o2L~pLR>1?XY0ep~m*4GKYV zk)Qlw8fvl4N32}#p7xf~3-D2Ux7jd7obTG&+{Voil6l7Ho9BudYuCG7@NJ;Q}T;6PcF$oX6SZ@#M5-fl_ zQR&2KBVjI^O>008j-&tgRt}=NpkP?r6_*omYB6D%?M^Eta`3PTYX&1O@GYr?L)$Jt z;NWXfUc*GdMqTd$8`ucfkCkSgB`vd8kcR-MNrnTXB4PuHwdzI;4dbnMP=b#)kG4vIPLd%Sod;zppU_3%Txt=CMl}HRX)e7vO zSv^n0Hg<&w!UJj4)gNUVDq9Jo=ZZGXaH3Xoqot*{s!d5IcBwJW6!fxZV* zgsT8VFjG5P#-Yz%GR9TEV$q{IkHwWsJ3|F5C^{1aaijNee&a1_SkJ@P-9N(4fv@fhn?w)p#C90p@$mfZKw%y{64aLlC~_ z+9lKDnWZm|RThqjO$n-Ggg7F_wR(#;3sm~qiRTK0 zmK?U=!u7g|9IIXos%ldvg05uR33MGhozMti$Vl47o)E0Tb|QLR z6L3ao_xV%O8oJyTL(r}`lz&a*H|;(boS-<*4&{%F5o&sB)z_X`+`lI(R$3!Dx>TUM zyz_NKMUI(V2KfM*zc(FIQ8Y4!TRz0hxGwhp<;SxjKBKdgIZavbKEB=ot&(9zI6O0w zPW17!jj$>IKYtDs%bWUs>Xpkg(a`m4=ypnO9)?DwiCujUh(btp#{I2Dm)Bb> z^w&g;oM4#P6nl9#MV}Qz0|5As7waN4(3bpq3luR8Z}T9EAm+|gO4#(ojG}twYs8)& z#H94X58n0NgZ{D>V*sN~)=%9==feKEAHHR4c9<3Pum5>q-gE;%j;656(=U${B@YAG z48Z*}AWOZaCkTGzvhNLw6&t+^)d3}py|bos^(HMP>cm(`AU&B}au{5*Y16sGZ_O!x zLw^_-8n^oQjtJ^0gJ?YfIda4f&yak4$XfuI+5!=oI}cwdn@od!RrpLV`4!w$sklE|`y$u;q(7J_y{NaHe{A{G`t$tedkh!k7_;Di0Q57i5D z{Zc(3xvc$k%7SC`44Ut@d2lR!140$*S=T#PaOH@I(Seo2P6^^7`6nJ$u2i*`Z|7Pa$ z{c8R^?__`xbn|cY;Q7+9CiD5QZ}yK1lkk$-T6as?UZ7D?pUruqR<`@kL)A){v!+#K z^-JRi*@kK4AOI8Ju2>{~>M7|G0{DZ~4d?-wBD=^kD6`GN{$Sg)lSq;VkjW^45X_du z0E=e-^1XphO8Ls`HTuUjq4=54TRb?9xUnsLn4X^`Y|H9e*ET?mR$RuFF6U};;d7CD z4}AMwvz|AastlC&+;!Frgr6Q27Q$>o$$`ziG@&&@s4SS@3L^pS6Te`zXZjDdBN4*XXbs*X8|O02mG< z2Gf&%{C-E)a`}|O44L!(^&RZ!^1nbxIoliOnsHEd-UB2Mjjks9|sbnN#t& z(FP9+xB@q86!Ur^i_^w>nz(XB2JJI*CV-C(HN)=PLQm@bX``Z9jVa|WY5vEPwxwHj zB5(&pVuIk%H)m85)~Wd4r(?cJbw71OrGN#_kY(V_FQEOF`6uY;tY~`9Ssf{F)uJhC zI#*x!y2RBPis@Un8qhE@{v3A#A=f63S!9pfpU3Pk3`3kn9JBh0{B^9GOfN$WVvV2Ii$ptgKE^fCGgd7A zcA0HBT&&Csg}b}oN9(q0y01S z3WHuyMIinn;qGStnD$W8+r4@~=?i&EGdJCv>{>@$<9WT0>88K@eL1!=G=-0R z@w%4$ZiuAe`Vw)A#my}j9U3HEuGFdS`EPq%WWqkUT_la0aN?H}jz@sRnJE2XPS z*}+{dh)EUC0PtH4D%VA@V3SsQgYk0l0yE~b>fyTwMf4qcq$jOM3{)i3i~EwS?1s%yx;s?$el;62nF{4V$cYlIFhzv zKN(>G2c2=`8aF8|ThF8NKxw;#88@~PvZX@##g?D^o1 zIn16!_RnGUNExc#pWS}Zypv-ovBG>YM(htB1!>e}D^;uU0{(MhFfMQB5J1RAEA1fL zdLM51HALbvjQ5VZTLP**>C_n4r`3A0P7gQIw6})#-;VJlPOqp~QhI^f$*k7A95D_C zU}S^!5|z;lxbRzADr&yG6&|>3q}J;_J6Oh3lUeJS;xWezK+s;sF@! z$?H=RaIOa){-Q+_rpeK*8VKli{eQI54p{+5YPjjU_jPiN=v1g6r?#OwL9fMB_N)D9 zRWsP_<|0(Ip+sjAZCUo{TB}{EQgWJcQSuh zcbqO>2-~EwH$2rXh<{w*Rkfv1M28Z~4{ezbJ1j9gH!x@bSnA6}q$NU2bj-L%qz<3qGQBk(LIv`>2l3HJWAj zv6)h`(y)FTg(K5k?9lCsZ4VA7bHxo6{0ESoE*BL=^XZwChcg3g1FR2M(XpK7s>si+ zyhtmD~z=OUx%28j|P#%wbC+ ztXDpMF{U&L!gTzazNiXn3yGaBaXOo1lRfIqd57!L_+w8J{=$flfyLO6EVpNx6w@A~ zc<({#bMtWVH-P5p)_tagXezCDo0NSiogV((rOK6Op#f z|I$^d0)}a4oqu6JM!AqJG~KC8jNCHRxO;x!O(V3QZkGHhm2OZRLcRSLe)$>B$^4~| zxYqSKABZh?pLzdK-YYOh@`(>7=4d0e?evK2LpOdN5>E`?;=vNBM+eD+Xh*$Km?)mi zbfvXV9(%+Is7^G9i>yddDiRtFRD8BJ#gPt*EBwDY-H*ULPT4gKHk)>5@9hIJ3|!Mx zE7E;DtgW3M7p6^GkPs&b#GZ5XJVy^Q6tgibB#yeg39=;tm5tB)o(Q*giVzD3r|%zdRM|McnCUDPbms@{5~l1vi~Bq2Jcal5s?!=Y`CjA*#Em4K#Ae z@mcIh3O?97eFhOJooFZg9&4l#=wy|FJmB=<*(%j6VhAb(#IIiK#%{iMZV|&*zR9IqEH6@&QAA z4_CP)BTzvXc|hKey@nRjKUt*sTt1>}hkl&D`{DRc4+7L`7J@1P@e;g)|NG`m^HlTa zdPvfpxXx~Xt1o|>mj@4N6v@|VOPzzxpO(huVbus(dVxDH0=Qq=Kbhq+ zK*b&nXoJjEn|0wnQ_dm$_c{bdsp)o_>Jl_bd5c!q4S;Cpq1 z@J$BUzS@9k0YnGXg(>dWR#Th4*FTdHC$pb@j>abvgV8W9qoq(Z9J5f|nH;35R{GP7 zy#HE*1M!$YLRyvnfbs!@da2E+27R}P;m#%}>Rb)jt`|%PfmtUG)6TCh8Fes1E;f&` z2R^=#O9BHXFE+*xYddHx|rr{;vf#9e*i_FjzLZn5MXHE5k!*YdZ~)p>s23zhaEw#&sNR`%^V`yjA*2 zYP^YPM9YTT)~nBiJxwG4CfmpBP1nPcOrKG$Pqo)kbw%s>`r)mlVrI*gmOI?LKsFC3 zAqc6(>tv3kiG->Mz;^|lOC%z7%{1`gR=j*65+MJkdVg#<6+aYj$nwh#%a;iXDT1tGO0 z%dpzCUzzU_7k+dOgWF>RHJN_ZVIQsm^}+uFc_-&(MI{5w43&SaC1Z0aZ0%LCuR8n} z=SRFH8PpHV$%e-*FPU3TeP)6)rxZ@FdX-(#i0pUgiosts>SIAT%Yp^1*WmylLXt*Y zFcA!1u34IR9o zTmyQFI2|I+Uy9)0;|o!yGocWMine3~{%4A-m=^~nJ~XkI8QPEUuAuR=qm$u4%b5?1 z6;cdNB&LXh^5s3kk6bB%8}|%@iD8T@0Ev5dA|Gan-T?J4G$qiCZidLDWho%aIjqec5D(KAOGk-w>J z2?REvuj2NUN&wS_{Vm$w{nnMg7+y#IqJ-)DOehF{-%Uz%fQJu>rqYv*EGFfS;Hk89}x- zZ_bEZ6Q z-{%k!PP1jFucQCj2U+!^o{e|cgZRn?WXWb&4=nZ)p|GlTz!4#)<97Om;>TJ?Ju6Hc zf+iJ@XT#&WCZhR#{E~UK1HA>AcWV^1CgTiVfxJRg+rIJIS&k2{{U$8M+gjBJ)mZse zmZ<;X>UlazOqfDk&=B%!E<$&NRGCPw$Px|qtz=zggPSp^T{<#U<2+^D4ikqD`He3H z$&7jb=s~Urrffm}5_2)~bEhKfrgwNxJ4^o!?x|a|6&V@^nl_(ZigsS&&=2`qRMIX! zFrmL=W`oi!4hNFzl5|&GrUoz_4zkF0fvLqUQ)Gm|K0o7XbH0XFS1rSZi0-2?sj(? zj_Si7v*=g+ATm&rCXxN~zVv`A>*u0dv-PKh=N~>mz5&C3Y60SPcSLq`!bB4&sTq?N z_Vw!+)2g?ldO__3ON7g>z6f0XN>T20G~v9FQE7A@=DLheGS7fxxi;~i^RqRc$gcjp zMj`@-pyQ|x>qNxo;)vKdQC&N^rfuyb6CUm{u(yf6aCoeY1(;pV=k~dKC^Cd0YxGSz zXQgyL*NmjtIS+47=l!FpVRq$Vb4eO3cG4kgal-`PAeaNCm_)WTwJ{N+??SY!dr<$G zb@@1S5lp8kdAhWaZeu!Dvh|_&`i&4z4%)9D;Thhzl-RPkgesWgUF{Kfxe!mC>VfN+ zKJa}uz9$8~vL7GoFR5gVPJU0cXzAEJD*Nmqw{l1k#I4b~y<#mNGdf1lJm)`&M>dvW zlOs6)PKtJ$2Tz2{Kki3iN|r~V=y`OEsY}DdohIH@v4C`^PyA1U` zYhiyS6riI(9Ifn^HzXrQ1d+MPbe|@D?$Cg%V0l>dxyM#gCGRoNj#X53flQE3bBi?SN{1$?4S?H)g{04CT}!e#|nny?GMa z3mKkxLh`9t%IbDu3YzbwFSpz64sNY6E3oZ|vcuSun<5mELoRUGMOOVPh6ZOuwtL zeg1oxSw3*8TPTe9tyZBx5`9mw$XD?`PnCL^_w-4bVJM>K&3%%q8ULu3`sS%Myvs55 zH5-*ncbhtUNkWd+?^wfrjo9mxc%+7ykTJ#xx0{VbxIzfcf{xeb(6oaS)n()=4_0G& zfhQ_I8pxY@z=m?#_ZYLnafLJ=?hpX6>qZcIE38er!veZIHPIk#(-Q|X(S#zAm_}q8 zJqk29>}p(?>zJlxJu7mx>Us&N=@h$s+u?RGGoR(odtHx-%QtxlMe1WtU@a;h%rFt; zYs;TD3hfkMbAUw}dZ~TO#$hkQxZ5E;frGfR_b`0(*W;J4hJ)k{8qHB~+lx{qa)g|} zCD`))Lw#T`TBOHGmU)V)S#l8>2|foCf7F2q;u2Ff*d+7PhDw5`>tfGSRy~<(HBPO+ z<)1EYt85cUHRD(*m(Y&wIxHOd_$HBvK&@!|tt{F zeK24f0yPch^u^#%lF0Aq-Smb+Q#D)q-*WT?4r)hzSwMP0Ewan{A|#A4(s4Hv0G&-3 z8=!sAs)hjVdwOk-B(Pw%-alPXuypPuMU6Cp=e~j&ea5$~{+mvc<-i6%ET7j7^NcWHf)gPJ1;Ym_PpRSMq4~MsAlS4*b_iQxGQLmx-}p9(asl zYMa_|q>Q`=&Tn-zOZL0)q<@dBi}}Zg=XcJ*PVV)>AJUS1Ac+YIp08;1 zUP&(V8F+Jb@X$N5MJ;bvHiUd0&*00W_wKbl2^uKKA3aO8--tie6pZE;;{BWt=RoiN z0Pc!wstd(VIDwPC`*~LZ?f)PY`CjPN6pCLSX5Rs;3lIv>89Yk+*jq>*j)cJgt_k!R{i%Q)0_9WN8Q>H5p)6)=Sj-vn`*HWcLKXd2s z(HnHH!XN$X&WzeRI?=s-BlM))oU>^-mffma8>7Ht7mCzQPLAg&?I`g4mfaqpB)aN8 z9P+b0f7bQ>MZ*s^H#_%!eEp_;l44UOw<{Ytt5rs-%r(c?y#oQonTmckJT4SW<~Ul3 zB3y+>Hk35xa^W8kO~0zd2M{Tsq1wKe6zq-5-oCpc(?Ad>-5%by17}sOegtb!!oDp8 zqW%i?eN1_gf;O3~8}AM=BssHhUVldrYio2mJ6jY~P(xKj(|!8bs5`vFLn1--6phr<1pRAt@M8Xv8KHu4SIe&fZ+iHkIFtn-r z79nUAZg)rG>X_9d?y<=6SLUW}^59?Y#N7_dYBK7DE)JHrRp=ybvAah%qP4m)ATXNj?Qj;$-MrEf#po@-ezd z;1SEh`d?zTWRDBYrdAcFFB*UqNDU&+{2%;OK>HI%j>C(i;OUbn^XxSAaV4EKyI|Wf>)hFksAcCyYWMC_hU*n-XV=!}3Gg`{& zX%55kW&_W^inoNo*Lv4F-;vmo(IuFz!r8$0;JAWb5drr9yD%fL!ph=?N zt^%lTe?w<51fC8{w65*uJaIj@ghm4Yg&?ia)*Q-jDFK^>qFyGt=4(RNKl+WIFz9S* z|F-%50gIC{9eGOY>Nf8S&jeagrt*KRIOR}#J@4YS&V0`D^c}pzyMGt_iEIaP38(Xf zSMXsah%0uAMukm|_t6)h6SCBx)f4^lZP+gg<8Hy%gCUchSzI>5JkmS7SuO5Kr*$jB zH>C?_P*yC9Zni=5Ma?BMVAR9h+|#UX^;mc|eWb2@T9wICJS)(r@5{u$*@3$pOY$SB zh8N&@9}$|DPd+6v5!cO&OZ-v(CTVnxB_G9U+qAj<#`h%|4ZavG1$%ihu|8)8M#|!k zLdple%G(eA_KH^hjieNi6MS9I;ns zH*%uVg+rfx+;TCX-$92oOMqorkBHQ^QAvtYMfe61Q2=K=GTQHD*G&|en$s~u-0$!! zQ5lT!EXD%_(>=Afa+^!oWIH@4sfhmB7_C2_jH0~`bPq@pZwARhtu!dJKTkqASKX;= zZ5qyMWt*r(Q^JwKaXvJe;EqpOB<>-39H~n+@K-%r5ODzWHPQP6bK}3ac`MCR`CbO8 z=cB~#&%P^S0s=-iVOUu(!Cnv0q)WJR~imyaZT zRv6k7vqu`h^@DuVemzbBW_jL-hR?@nZ)k72Z{nO)(KqodYL-9#@i=RYi;kjya>X`R zC&W%pIdq-Et|v}bIsI}PTfg_rML#hoizobmeUTEDO2Kn4*1a!8?OE;D7u$;)S1?9l ziq!Wjxi#BwNEZD3gIb%8e`~gPlLPh+;+YO(@&^lv_DK#R%Q!I|>7-4reISPuTf}_T z`$r?D_mk{eY74D;XFDeWV=P^s@1o^UB9Knjd5k@`DoM0K`4g;FmA*BQU44M`xh;h# z-dD7Vbh{AO$Kfyt@}K4fBwtHBUiwB$KPFk^fkhISA~v7OX^Z$TjQ^HHg63ZVH0wPN zolREElN#?5`7=oc8hpjVxW3f*#~rx!Tppco<2u=_1=pe^75exL-@)KK+1LEIUue6k zKqBS)EjvXr0HsH%=4CBAjZ1pp!u-!N8>gtbW*#0aIZh{|Z+`n40kaN^{eh3`JJ~vz zUvEE;<85p_RqMQ(4g`y4L2R|14t-fH-_xXiRyqpp7ajJzjyB&(q|PMU%EowePnz3e z?0sU36ZHGdMYLST6P%U)oV|(sF`2A=}LNwfkvXA8Wx1fZ6E64`>MA7nrT^{t8rJnDbxWCB%6;J;`$p7YkhJ9Co4n7~cb(7h?y7}#~j4?uJ{>4bxRR(TJyD$xwrGc1m zxU#W9Ea5$m)s$tm>+3_UuA!ybS+^o)VsH>GqHGx!URB@~2u&gRh() zSOn$;yg{uQ>eIzoYi7Yl`?es)y84QMB}b{E&B)^;w(y=Z(Fxai$bEDguZ>tllXu%r zNYjF4-nlQ&xB2H0@&zC0q;B*W!t)XaLjW_>JHo!J>vE zR)d07`DEGwJC(d%er{vCdIm;q!z-Kcidw!*#*~2EJ=)9eCZ!}3)cpdfe#E0GHkK*8 z`%rjA{xoN)-}Fa6(+Rzw)+UT~YE#h>3D4Z77B7@2M+>9y)a3#r$cu;r=)SgFuG<0e zDs+hF@j6NR19=|*MuCl)jifQ;^paqxj#cRQtsRFp71W{ z8u}3TBcCq2fy=t|QA&_nudFz2J^L)pQ$9!9HZ+~#9kC*M1zQ+*HlnG$yV#KC@M&+i?KxK}Pf z42WFwt5b(l?`bC_=qDs#+Vh*&r5buP9qTR*_2r*vm5=h@p8r+2a6Z39i>evf*U7so z9lkE7UM>tb!k{4v{~k|MBSw{UdsDOtga;=n$dzJDNghaWA_B39ZgTQAdQ#t3{#4xI)8mljj#8*SOm$-6t!ggHHY~6wEC|dl#C`UB@L^Q#DL2EL_1 z*>0-Hg)mE@bb5D+Fi_u^^Ro>Vi7|_tKhytV?Ja=fY`Sp40fL190fGcgkl+x4dqVI8 z&)^o^-JJx2!-ofV8yteW2M;>H;O_1)%zerC|F`z;-mTiIyHis&Z_P}d?mm6`SU;zm zm9cgA-sk1i!29Cd{i~Nc78fTHkmDmsXX1h?9=%sA}K-lWPO{^CZicMQ_vd zP6sjHIhb)qC+NI#z*Vb499g;Gu@Zm*Hw<{-2xW$+sigEMt64z?tXYOE7zAufn6t4r ze17ac{}l4t%kP()&LAC0hc?aRw?uzwCvk%(GW^l&${`_m?)2EVI}@?uuQoC1kSo<0 z5-{m>4Jy^C<3oG!E5%ueLTC-5zW^T+Loe9=#*GiQxm~wR{SC_gV!S_f8-QN@2json-gZX7OD@Qv^^ zy>~B-$zI&rO$|EE!H-5{NwctD>uBa9ZVF)?z z{faHdG`H|oH=W-nVkEZOMx($1)2)#-UWC*?ZvD>$4&+AW^8@S5`{i}q%#7xtOJ#Pw zF&#;j_G-FVuEu1GYeLqlN75@1uJ^$45C1e7K9*&eK0`A+ zL*R=!d3-!$+%!1~58TV|j91txX%Ph7&8`lZn#q=Z4l>Dr>`YChnDZ|@ypRQLZ-BWF z0q@I%P=9W!?6)a(DvE(Hw7tJ&2fV(F+P|cqS(4z%L(T8GzvR}{Pu24Ov$7*KsK zOOkNB@2I5jr#*}IDh{tgNNF%i?*+qGzh5k9unhT>d{`YZ2Aywtc8`k+2>3I`KC|^D zpD`wGFv#z;9j*NjGj5&wSUuJEy%|JyU<ZZ zmam|)sy(~RxLdbmmW%~0eX6}evQyKoBrr?0Qw`rA7fH4-0z?-_rxr;d>sIRWa>^-y zi^7Bj*zdxpd<%8TmiNd1fv;PggwP$~E`AmFy#D^3`NpeehqrncFF*tD#K;uxuQ0zU zdzR+Uc6=^(Ux_W&?HSe`DNPMyP^`bTvb!0Q^#7%Ox)C3pUo&BHrsv(*kT@IIVTF*2(>&EyX#8HM^NIhwSGoP<18Rqy z<;49+$}B3D>tc-HJ=GuY7|`JW>}L}4Xy@5GX&M_o`j4gI!@sLv8n?a< zWg=Uu8rEaLU9CE8Qlpp>VO;uNL%+0rF85yidoaEQO`LIIJixbi?Z}FhHF#o}6+#2} zou__;$dk`S#frQe;n=v9dX?ZP@W-)Kn^Gp!F#UIXq(W^f$3a_seXXvSTmP*FfqRp<&3qyTuojKpZeXA;VKQ1D3ukmnUJE#t6{u_O z98E;>>oG_I&bgWJbHM=qXTjVIp$>Sy`?jKerD4w2ze~P!tlxN?45*d{3X`(PI*XZr z51i%3F7Matn=c&LU45wXr?+i{VjZ_H&{Bps_iNNkrqOEhB?2d{DoNx_Ov{0b8A(g0 zJf?W6Sc<21bSifD*6qg*H(fYW>NiOYH4=&q!;NQuNIHjEpZ{aHuUR&`5bS8<1D4?3 zp&B8yh!|OqvE*v>uCwb|IezE=;Gfd$c*X!AE5+(IqD@UPQ4^&s+`i7OkMv{yulva^ zE?P^d87#tS&L?{kww^L{9^X$MvDt-?N_7@PDclyvZ4iCw`PG5vwDtU(=NJKE9fPAn z_=J>CL4&@x2kp@W0sjDq5Cvf#|31DvA5A52^>MYaWUNfil$?kD`ELN>V7L4`ZfdxT`vUb)t=Z$pPtWA=@*v!qvA`(viSuz*w7p2i-;rdd$6(dfhm@5 zUHOP{pZaD|Ngng`T;Ix14sTgyuV?kUB!i9rob@17RWjei-i!tH;%viEwD0<$WZ|4! zUYW`RTchrn+HT=3Kt@4(fZ2!y4J3)vPr_>wFzCW7wtWfjYRLZIVbGVMq7QQwHwt%r zSW0+V1p6%4LH9SdUoUh|xRdjZ=lij0LaN|6MJ3D)I95 zj-yF6mxD^@aMus$*}8+qt~0&h2{HM<{&_JvcjVlQ5zA?lwFV)W)6&uXl%~ zn`XEbRzWUj`m3qh&XV6Vxb4A59UQRIoV?-o@FQ;zmzTUI-60cO#NIdXe~`SNAU{Oy zA%0%0wq;X`wZXr_0l-dgB;{r}7$$lpPwokwZoA}_bJ{p`88+c5Dz^2yTBCEtrnp|a z9hM`|aSX3yX>#hB+e-K1)B5w3U=JVga1&A9xOcMbY)PTuJ-L z`3?Er$)hU=cY(LimmP;QM|I&iv}?*3xdyD|O4rInfo!YvVS9Q98;zY^CM@KC-C-Zu z+w8TdDb;Lp9|-VF&R-`^cy_pi1-gfO;Tz-tfVR|1U9sPe3i3~J&glafHXMFD2mNMd zkw040V2a1Mz6VO*9I(RDDlI^64g>W~QaEUS@|YYv2~)dH*cK7mn}mdTPeI=~$ANtQ0$Q0jsIc+$eNpTQ+p(!_Fc?4L5Y}?Z|W+TPry6RfxX=X5IO@6&~vdT$UYKaIlD|10Y~`SXiJFw*d^@az5`b1(QoNG zM42)p@OWSfHl6w93{5&qBqinM%FB z?M^ydTue;4cV{MaM;*DH7j)960{v#czq`%lB>~uU_B2$dI!SKTfEsvfes-GWnC4R4 z$Qn`snqTfTdHL!Vt2ke?%^+= zY>aweGLA)mO%v?VDoHq-ZP3?KQ`vF~*ruqPqL^K0TH@cyV;J z<;ZZxRev1iyE^1vD*sptF!4f-(hxr0v=BXZ2d~q;JdI(|J0q+2a-|8dnZ+I@bVyQ@ zRn{&D`1xH*VYbOa{@}%uFze0thVt`IN4K-%V3xr~E{?`X-_}(-Pe84WPwQY_L!6#l zb@(fi1qXcVIWt88eZ>G>dlct~x1odD$r=mj663%)Wb@F!^_yAm-!p^2N(my2B2EDsx7{D#@D5Q^=HNyz*foxZq7646!M;vN57v&9?6K&;}kA;V)7V zV*xP`4%caJwJ%aVGLZ4byRCymqSU%1>(yq!DyGyP-dZlL@N;G;GZ}%FAHVwg`UCJ@ z5ODQ)5IvIfIi^g6pFpi<@G0;by`deAw`dWnb&gBX;UWt$RA75k3z#LLIQ zVE+)_2YPj8lpxD!Yhmtt2Y3v3bMDV#V@MJcVhVXl&e2d%A-p6S338ug<{jS>fGqY% zM|6gOyDVKNfn{ImJp~}}0JE`o1pUcMlwf>@_34GfOOVt51LE&A2|@L0^`5$%lLf3Q zU~{fdC!$;$szSSG3W2MiJ&!<5msE0&+K#P6O-)l)Bd`TlK3h&$w|k#DmMHwRufwol zv-PqB5ii1?<{HjzS~Dl2)d_Ydb6IB5=`<2V0zn;O3=wo6m$HD3qJhutc zwY^?Xp?VrN7%r&-a5j8L1@KuA)vik`a;nxO?HRn)%yL&iVQ(I*=Mi$1F=lX8w?7uW z{QVO=@#inZjSGJ}yne;tdGy*dQK|7N-8}ii$5-5v)wo7Rz8Bu);GWl?;&>QWtKfCI znR-5yin>%nOI@Q0pIbk9GgGqHz-!`l)9;DnYfI3?L|^_sPdSQYzE9dyz=CV;Xu3?) zm+?cJ_H1!%A1c~4{#ddeg3o2Q`TeWlh^M|QI~I`930%X;tL|q(e8H6)Dp^X<@)x%Fx8}e**)6gK3ibSzu)2+o+we$ROPtn8VxR# zE&5d*>(I@be3t(H)!U9y61)iC6X9rSh8OoOrhP+K`PX+INt>oteTy_Hbw>nYJyT3m zx%5G=QGaBiQCek$c#-s`NRG5TP%WmujbEZ^r)a`t0QfM zf>#s^zpRf=*+m*XgzaY`&dC0mxxY$my%H;F&sRyl&v*xNsO~h@$)=kW~13f)@ z<@VBOfo08H)oG*eUOV|@5nm00@#x12vqF7P5WJw<)_5(e<7?BOWzfH`Vt;PF|2(#@ zW_2L_=a~o>8|Ocd6>esB|9SN5`S=~>KhG?bq~!m3)Ia&3T3v|i|DAL~ zWJYUdi7K~AzV@D3kGlXISVbU+p%D;wc_B;L0XHx#FT)Y|rcX z04+}(N?t2kP?S4+3e0nPh9=;j9v*pZBfi#`8v}ysIESY)?g-Xcq|WZx!v4; zps+wqsbfVXbb%ka&))C{?zKIf2w-;j)5k^=MAF-g2C5B1%*Lv?FE3p}`ptFb>VThS z>W!mA+0jZca7~CYCKKsur)0RB_Ot zmE5|Y1rA~wRGRGCJ-;HrIqa(~ZeHmVp80{84!0=3o)KFd-$Wx~8hGs_qVp zBi8^g4yfW$P_LC9jYKcHD{iP-oOkCp!b|1@tCz3!^Uu`rbRdV)m4Q8RPGZf714aR zC=E08d=!|Ja85X9xWRl5}qhb`*+!v$ITFIE#PDD`8_U&%Bj2)zAo(FSzrX7Ss{^ z_29VmfqJg;C|EX6_nq&pBY`A^sP}6gCIiG++qwHC)eZBTcg=5f`9sUe=wgyqCnIj`V_@!tPKSUi- zC@T@4^IaHEhSp$}4}Q=pl1^5;e>hoY42>-9rpCkL7ns{3d{OD?0Lal*_L$i@mC_hH z?G*JJi1i=|b)yG7p*YNI)Zb8?HeNPHWy1DN{`vk~$1qr4yhg#iq;zpyMm&1uwOCe# zN)wUq{)~b+Ca=v^#1S<PqRjxMV<8rloPUA1!rQLP{48QTl>^!zSRq zDjCF2pNMx86mgTnTLjV~gZUz3rMv4?#$BoSE>Aqmw{abvD#{B>-ktN}f*t|^q49Ia z3qw2%0$FzV^euPYg0+nid9mm}{rx9_GEvZnxT@Hkj}m#EBl4P_0la#tZl^;%mc01B zixti*`>qt^`^?OQe6NW>prPsG58f6KEdvM5;RDi$>He{f{{HCTbaMWgC8$>MRH#I! zpwyo_!t+BNfgXcOpAR+>s%m#i;*)vadqADznb11%Vy6xJl5u3tw2;;%;=;I7QG@VjkfYy`Wzrw)6`A51NkRBZD zr@sf;OLqL8D=GTfDqBb%tWA*)r+=gusIk3S&E)_n^WOxD16;%-S+?BW24*UP0FN~H zi>m9vpGNQE`tDq=25lnpZQs>%i5f{2ozekjhDoaF?xn4{2$*;V>p(LBi$ zH|T6t9oyRnEEXFSiDRaCb{ZCB$;Z zn2L+j$SEvDk3ZD16cv7%fx&j5zh7mSx84;QYk#SWwB99GwXN~Co0@_|ZLxdXbpBp` zkGKP>>aYW&3V}_JA5AUdp3bNB`}6k#yrB}*-mW>B6?|NUn2lsW!wsnbZCBpH)?@~J z%snUGUEz|44#bX4$%FG*DXi!~bE>-7qh;!{gDx*WigX%x0UKo4^^&|6>rs;cmVINO zpV`hnVM@ciiKEK#%ve6sl}8rTuB`tb_oG5cM3?dhg<_kYAV{6fOB_Hp{C)i&dLMsj z{2w_s?doj0jkgN}R2l`>u!xk9Vj!2;5yj44uHI^3t?BXOg!<8y!efL$R7)QC1?jIk zsJ;PLF@i3Dh>7x>vc^KLxg|VLB1m_%RfwBeaLWdrO3-~bcC?ktxY_d>K6)@=I?P;N z=+(X!_DR-c7`{4pp(QB?48OL-1w5tavcK3T@B*bSR@r1I-S>K?AKe#YfD1V48RdZh5O$2Ni6Q+94-D+jV;L1DWRnrqTc2JSFi1?H?ChHDv_VR zN*?exj?0rF5JZ)P_A{8!8&#G3QL3fAot+T@z8su>y;9B;0Resam`YjMPj|W-T4Bbi#ydK6J}fu?l-`& z?hH<x!9ZkwhNt$Cx%V-_`< zh&-PFfHhH;&}zqWAUP+<@4FJSd9e04h~!uSs@l(czvlXSMI6_!^<2YTxBLKUdXybf zc|@d&fwHWwPX{M^%S%e4cb2zJJ3C42Z4bH!U1dx47l2G=kP92kvNsIT6WPm_E-&g~app{?;pPr`htUa8mMl6r*?(XKU zPojn;yhx05^Qebv>AD^X{`xvGZKEULviry7B5F^0d_1JEu%4EiRYz3c#l^(ND~# zo5a6$7F_YqMVsxIS&jIwozVlidw4fsSnc2#L6PLXE3+Z8d8apOi>=XaoRuhCgPTUboIlV#}_VE?`Fa=foi@N%+tL5mX zh-UY_5nxtCgugz+qu_J00#amfsu22YXn~NZlbtBNRNqyBZ%m?Ih2_zQc-b1a4*C!N zNG%>tCl?nDX>rZ#P2d9gjIgi}kq2nfs3^Q}M8xfHszYic;g%^I&mm>eGvh@ixo# zwF3!p!ua@$NJ-#CS>P_zj9>%9?EQlSk?ZXVfJUEuYO*j~v>OU=yH+y%PbCxbG_S}+wF4cJbdcYvx_&`r=g)G`{YGm@L!O;7nh^y3@$W2Q}-3k zNb|&5-ypUlht|193DE3G9bcT3H5{%ToESIC3%T;vd7uNUs-A$9{Eyx3gm{W% zKwh3H#*=SdO}tGFjnAVsbL^uB74DrSeP<#U?f_F{exrt^$rJSwvHt=dQL5Q}d+T;I zUyV2(i*+=S_dJ%_qbTKZ<%RlquU1z%#wbq^4JC27wbNWtoSm^JzA4Bpy}kStFDq;` z`DFD5eH2Ir{2_5@aP(^{fAC22R#;fj2E+#k2PY|M_7-K*BH|%bomTk^*qnjAdle5hoD zdAb!i5gy3IFuXWvKAgKq>1Q+-%PKT?Gc!>gImn&NUX2+ae;2uic7AYi*0=-O_(0DMPV;r`1eUi0kOi2kVkGkrh=?h_CYfMW2V$-u>RLu~^nC0jAZitHo-xRH3$XtG`>!o+{w^u)gCc&Q<7}aT+=R zEOI`+LimJ^6m_&lo|9YcSMTH-FPl00n>S zY&5*h<3MsSvW1hg`c4$TO@B;TR4?P2Qb#1Mt$rlM{EF;A zAKce4MWO{UUEEf(BxH9TJImjoqUY@&JXdS8vazF-S8@m~*jiYfDk?zs_apt1e})yd z`|>`+?E)wGuM4*7mKtGO{I8>p&6g=z%*w`ga$e|5b(g)04}V$9J^t>UU?1*-!Q#*u z%@3h{ZP!Ru!;Wk^;OGQR#G0#kTx#;7>7PRPYEdtP$%?xl<%NZI(SuYxn1Gg20+#*R z8P>vr4p7_futo(Np;{_#tIVvBK4dALJrOT&&W;H-l8?MflE`5M96TK@^hlDumBZT2 zrgGAMW|xg`DgP_x`fsVTj%^mOsUc^LMu57_7}8qbXCXV5E=L=3j#A?%@6(a1=eU3; z*_5#*mcQW>n8Dp9^R4@C85|(y#0w;~o&kLk7T#m52P~EAqTAAR?^1QJICo7=c9p|= z>?n|5@xD=Z8qvcPlT=AcOUUfy5OzmFo zi#owg44j>7f||54t`BeHR{$TkWC#erVq#RJo>3Uhd<8R2{gBf_LB4Nv-7%Yq#xw>F z3J=G`#)0+J{x`R%|6bkb1t1QOum9iOc!EkCkp5fPWtu((&^N2W%*8ZyskcO5z(6*Q z1LX^t(hBUMO1X!DR_lCpQtBDCbg)=5j~u(;%*hD1aZTcr>1uu5EA0?kR6Z=8f+)ny_b<$riRd9uPOa6*YKsEEL!tvtLucn5p^0uru?_e6Ad6ZC0%x7>L zW<@9_h777gjye676l3AiDf#3ixey1TQL^kQUGJXhCPa4Bv3ooGcb4z`2Cxj zJ`jqL)+*Xtt?(m*{P^E~>FIck@&CU5zod1$sB`qe6^lRzexkW30i1jLVm}+XwydOi zTw_yxOR^am(DV}(RiWP@K)CtrC1f?T1Ic6;NF%W)l)1zUvR_{x2)O2%ZhAco%ce`` zOg9U-Ya5AU#0CIeNI(V?0&{@`48_su!ecdtur2(hNamtv-M+w|ay2OXRROo#qW-4+swxyL|d`w8LRxx!^K)koMMh^w4Zg^eeu^0@NX7O z2onf2g8{^bLrO=76d?@!+-oCEFTew< z&K5Ol<|!8gd+4KQ#v6oX4qM5dCv#ZKA6^*cd1?cv3ob8O9Om(v^5exPCZzkeHS`6R zKds6*ImXtfc;1fuYME`@$!e_p=uhFiX^1Rkx8KNTtUdmTC`bAPIQKSmr z&o_H5w7_YVJ*ijSnb=wThhi4T|n3Rs3T4y5kb`N5MPkzgPhE4`I z3DB7)lE0nLF-8zApx4-yA^PIie$V#w%=QE(*Sw<6hQ@=z^sW!84!h9hRf5h~Djksv z35~r(&j{a7Eb+;w4IsZtdv)0X6Jw(-^y}oEAS;E8wkoeFTLIky<)VXJWnaT$@$W&I2Ug! zVvIm+%IdeGTifhd!exl<5@yyHH+C%r&Qt=JgN~q|DBmY3xB$J)&&#(9&igValSh8# zips=X9>>o==_TXFh;|He(mZ%5vKET3a~|Edm@0Ej^Bjdcf3C0cP4{*?LNz5lJH>04 zRCikV07m6>Js8M?Er}ABG(%{)W3EQ8Z9~0OYC3r_wPi?Nj?HsPOlAHI&`hkA@>Qn0*5 z_)@k7C+l|e*P&x6B9wX>8eiKPL4KHAeC-gUEy7*)lJB6OIgl9SljzX_evPaCjD>@r zHnCfYj_<}++(Ys2DUs&GSSLxpfZw)Qs>mv@*1r_THuikk(oME=_v9T4i--)Q$hPqM&?6ZhzKx9eUhi!`6#+I~5! zZ4nWToj=#G1veUKv{8t9_3j|_@e+Wc% zf0IH1dWP|sQ%lKYU_57Si(CKG37c43l)Clsq!Z-2Vs26gs~RO+fg~^bnUDh@ z9?4stYKo82K9uARmUtY*8*PE(CO;=H@g;PAb+SdZ_l;q9yrhDJe&)^R^W6cCQfwI_ z+bb&%gST=eYoPYFGJ4w7j9D6O`!`*6zDN9bX~Op;KTeKMz0W6fiYm+NEzZJPcYHUG z*?xQl4JGiafgd*+t;EIxS;b$>uKA0MlFxcS!6QstvY7u2%BRDBDp+Idcrp&2{s3CG zmBKk3)VmLEd&6CK^9d!toLFXGot|309XtKh9PqsibXl+6haTjL)#;Jns%&b2B}!00 zB+A3PP3m~qnLKHYbIeC>jSNojozB0LQ;|(rHSu*bBkJXKSf782pulaP8Glz5Nhu1> zE781IYg`m=dTI1oYewq{m@wR8&(_lGvhDTyuTUSXyJxKTWBKli%-^$b1lJztYtYDo0(^7xr;ndoI^}UUuHTQ)(|FeOKM)R@Ztm!%tDiuq=2PR; z1LV6XccyYU9Fg7|l8aQRwQiWN@<9urNsg``M<<|ddnMnc@~<#Q7b~; zo;aEgf(aj22XmtLT3mJha?F;ZVE@Pa;kv}ocS4OkqX^Ee-j^ALF1=ys@5R$c#$T64 z)HowwDFgwog#y4~AcK`W^@#(NataMuTV3i6k)iQ6uoiD}o3^J6jvZ>a8xjUujg;0y z^d;JKEncTOQ~TXjYyjZGtysYA?$8mv7R2MPbK6!({;m1!O?JS?85SZI*e_RI&%KMp zb11zKzXcR9Lc=Kf)oj}JnVOt){%ROM*sZ7GNuN~CSU1$v z*dSX)(Dv%iH=zoL%Js&&z=bkYE42BGp%F@jS*@a_BDg~lK<>p5MV(J>s5xbf*=!q4 z+mrr=i89_MyT776@xc8ZP<>o} zVnqVyJ8S;Cup5q1Cy!HWfJ&Z>+pp2E6hdV5#D((o`L;BGN+Ga+3j$fHdKmNK^*B*Y zR|N6y@uvpMmhJNx0jPqCmjdZaZ?E#U2MA^tPg?R>j41j*)T?uR5eS*~v-?|96@1%@ zNE8sl|FI_*tL|H!pNhU^&ID)vzB=i&TDiEW)P{r8FbY!MXGR{kwDF)R6nA~tnDPD2afa4|73`E60svI9>^LgA!7o5Qs=Fa^z>SJ!mcB4e#g7IDpeI}DE?0E z>JK9sgwJ!fjF&w_bQsut0PHSEKhLdZyyaB2Q75-JR>!c{K5@1{hI)a?_QWDDs|pF^ zM+mDha;Xkon@wM6R7ma@2pdC{>K!}IQ-+Kn9ztSP4FJKl0^skvys21)-b#4#oZv8iwuq#HB3_ihMQP2FLZI+kF2|OcP{JMfDLR<`wq-M z>#P6WZ0*TgVgL}x2MDNcs5<<3T$Uiuc$tD=hFoK4FD~JDmDaEyDAN?H1`SL|87i40 zcz#1`Yi(m5RP?pf6JU=rkizO=E-vO14DvoQ&CdhmQ{Vj=*b>0s6yxLJXSP+~Ah)M` zKjdkT2nq=0o_ypE!x$!T<6qaaxK{+Jb`e=%Lzsl(mJ&w8SOwz_#)|G#sH#Rf{q*cq zB<}m(wcekr#s1Qy0MrLjuVM|_UzM24G#p**=x98gYk+c}mKci4A32-~=7)^W_eP=V z8vt2}7euhv^t=iPV@VSi>FP@$72D54=oioLv2M1$abqlYRc8Ajo*MDME;*{ip6}Kh z6rZu!Xu!6|0_ru`1W!pKZXd(iI4r3U>8*(YMHCCTCZ5tjUd|8F=k0Ob)F-QSOMB() zx3^kF;+9i5kb+J0uGp3?SSk;x8=uWflwqGSkLbuhJ< z>2bnU8SdunsU5<0jS7(#Qe0@W_nG^!xnKD)Mv#lRO(amfOqvCfgH2ZyUQWw2B?L(7 z@z!BWr_g-jS~ren9ouOQ5=WOaCz6Yr0JXeQw|odB6i_ZXZ?_;!1{8okdZrNTA}P1J ztxAE-t^cJs(pXC(`JD4_--U$c`=VanZ165I6@%G@1Yfsh20K%O_9{)cYBx37)?q;x zUY7*Z8be}x*RNsnqIf^g2W!h0DQ-p{#r4_f-vQ`@tInQYsCl(mnVG)x6L3qL&O6tH zy#19I6_(Ze?8N!*BMGUxLp;=+-~W$d7xK3GfCIA)xy=y9rGIJe;>s1D{s5K&u&wf$ zDSilHblc3{YBQwm!A;l~ng!|f-XJ7$)Y9FOA-TDK>aX4@5ZU0iKE)o@;>bsBK9xj3 z19RC0HUMYHwaOl2HgE^?TM#wVr#7ZB#!o_qsN|-F9H``KyZFqE^&06u!~6^*k6mEo zEYC0hIr$Uso>I^5DA8bKq>4DhQNm>^ik+sAk)n|;?H*^wV6F4}RB?SJ~l^c-IM>Sj{(`qL!O^az^IP+D)-@0h| zDi=#i69|BRm~2r^tS3pbw%bs-A?TbllCA^Z`%U$Mo|*Jb53@S(Zt{!wieBLDv6#>| z>fG0Mts&c%F0>lW{jMVdG;?iF!oR{Yoxj~Hqd?vEv+M`Ov}hS;T26KfeTeS6{of#J zQ`+=rmz}c=bF)qmM%>s`>t=vhGTP_QCGq zyioN(pgIzTr~AM3CF2}h17dljA~^Od>^Ul-{0$6u&0m91-`1KRHLGqt-mUdc23eom zE&Ye^ilf6ZB^V+jXMMIydDs00pd#Hm!t-+7R&?o0Z2a< zu(%e<4#)jy8hmyZ{hO3>#N}no+eFBVw5m?vG)a)3@$5<8j|5cT_5Nh%v}+`Q8?3b| z8R!3uAwghTiSnj^dV2F1=g6L;ZLvlBDb(&*hmfSK!XN< zmDDa+U6XB*Q%*{|x_`+YIThE3s-=L13I8r&>HczcR;8pG^ppML6zOA}cJ0xy_ZX+W zjkP}`$D6(D1Ly?#sZ$Ts`;3r$M^PuzD&#JND3%|5D!4PN4owz~!YvTO5R^)SZI_ET zzcCyRn_?maRFlcTe^gVkaQ)P-iM? zlGbC;?M(I$(Y3n%MMUY6?|=Z7U;HDIvAuC!S0$R31(L+kixU>A6df?yViFFh_x2%k zuEzD6@-UeL4hMhf)pmhMx)9%uL1llf*g?I#eA?;}Wc}es)MEv;5PPxoXJQ`A)3}$0 z*RK>beq@G}2t~UslB@we;fLL?)hU5ZtGzuWT&a2m!UFC8X~4K0`2~Ps6Wb#3pXv)e zqAS&L!S?xSwN>35+WM&rzXOrWW*CQbwX&E--t}Gs{y;5|tZsftRvDgkb_0Ac4Ho6C zEQP#5m^cxzU1~`HXi`AynsX-{;rL`Ptj_))oe7hlNx z4~H<^5%DM^Y5i-PEXesP+T0h9Zg9S)rF*3~6JZg}aIrC4WJkR*+^XXQoQb#WfHMIC z?J67TzNSXxmxl8O>KQRpD;sHZECShIeN_l#imUNCu#Dw#2|E2MuePLX2Yhv>|IFo2W-L%q8 z89fP}Ijan5y%ts_Lw)-ffLijTT&V##4M(EmP#%A6c=dIxN$~d#a8a|Wb+ZPLCI6gj zym_mmUih5P+4ID4TBAg~=|Lg4$;Cp`#cC;|tpb>7>%TLNA3w|cB)8NlW5D5POy^bsQBm9OrpOxgMkn1HB+3hkBesE%32ieASgS`USB4z(6=(@)TgIR)Wg` z3m#^RVYqPgANn7`<7Hpdlq$^sI%zA{{cXvnw!iCe58mz)0{sY8kT%jCq6Lm`7&$U| z6gZ-au&3&x@tUP2stX5;9(B-8k0brTr?G7K(Vm5Pb&6w-Sk}6n6n?{1^^a~E?x&nF z<5Ev8>A80L*7mtW6Jsoi5AHMh}X;vSO0*Eq4a$+)ylg`|} zz;C`O_5hmm-X5L-9b)!mvJYg$*hR-9533tf0)JJHlS@xlxBge zoipXEhP~BzKLphNN)eoe6dF318s(M!#yn`NO+T&2!r8+~IYGthOUDR7&{Y+{IXYkh z9Rc*(VykoN_wOz0w7cW2QwNjhb5{^55A;h;^4Vg`HVjV)N<+^2xv z^g&(fp*pPSqO)J2%7&w;pok#A{jGpEMYTBb8<)}#dW!)Qu1ikLZzD#k+9p2<4@CM5@B^neilI3%stLi(O(GYs_gGZfnk5PjR?2g7c7C|Sid4CG-KYi*PWNzc5h5NWpK894tud_g zH#xlyB&Wlvh}v0A2su`COJ^&0r;mj6 zd=w#)AqUo}kIoxk7sFe&#Mlf%oR@oty90oaPWEq3mp?{OFg^jiU7i>ZuK#V!?HV7i zMlR&x(SMH%03SVF%&*2_)#IFw$=7$=M?IG86&l8jiSxliU0O*d0_DL8PN4(JY``Ok zldI5`{?m?qCZzM@Y@Ml;_Ra%&VnUSADeWE$&xgYuwZ%)q@`;QCorjgRBk#v3;jst6 z#>tRU*w5b+TWo5K@fH!AGeu{^hSVY3^F5Y5ba~WSip3Lj1;+SFE;c@S*|N0nZbMN( zn^B{m3aQP-jpEGnv25o)_s{N}mOb{t?AaHvo1H~Otpn^?dc6q&F`h`d5fg9*nAWS4 zDCwJ>xv4?Gys7g5$f!+*chtGYR+D=Zn@^v7n<+eYDP0RT+krU$n;_mOqW|GtX%zqc zySKZ<#_!kO-Yvo!Td4XG(Eb72yr(qXN0 z_0jF3rlkG#a1N|$#NcCqLDAkH{>}@6@-iPmk45=Oupb&=%7!KbI6n4;lN3(u?Cn*B4L_P|Eol~e{8ZdCLSJcmjii;tst&&hGxfD0P zmD|NLN|r&&>;T&R5Elm?h#Xdl{k6uK0W|BVdFf-db=aenCl$318*3Q_0SlRhaDe0| zdx*I9nLv0j0O7Vsa3RKc!scMwjDzxLP)0~WeBC^Iv|#%rJsKg84Kl$66CPUjK~NVP zQ)4z`*B7_7IiGX;h$v|9BD;i;LHgG>QzIvL3w0$~wPBds%5c;D6we1+PIJ}PXB9}w2*1VAjq5-r(BY?CGb zXoxe>b&4X3^R}o?XYojRdN1IBg#hURKaanJHeRZQ{HE#7)hgWHp=bRB;6wGp{?4?f zaGvb&0YY4=b2@et(Bp`)ugmpTK_+8?P4XFhk8T{}-z38;>hVo_3qB=9^|+k^9Gs|0 zJ8J6=)(HgwZy8Z{ka(1A7ULi$<%Y_^KyAlP4qZP{;9s5K@d>DDMxrEt3=tECv$z<; zygoJ?=Vu7ad`cg_nM-_=ndOe3%H9->f>VE;O;l@``g@4GZl@hi=b88P)7;fQ zn&tn&)mH#j8MJL5Ktw4)q`Re&?hvFTloF&HX@oqnFwdyttD|$t9P9#$%r_XU7Zh=%*Rimw5t`D5u%aO5K~T&}>km_BH!4QX*X<0+ z7oqhBJJ%3Pzm)4vKbF@lsLFbb!+?a?W6sx3#$58D?l1DZ)9nRuNOfe6xJvx$YZEz>o6bf9IdHOX)lX4 zBPW{&iV%EPUaZ*j{O>prNWn+@gKku5&fm0}W%6!hxCI~W%E8tyDv#Jq3jy?VXyH zsWFU5rKmA*BQ+C)5y)?Fq2YKl34d=8`qgUDlL!1Bhit8M-8Rj25`SNhHvzb7)qe>( zKKiRuGn%I!K~-VY2Vbc&cXSeFet@2Y`LL?Qf2%OhhHX%mWfoF^MUW1-=9z#=!TnqiDi}a zi4qU%d+Cc1Y72*?HN*fGnH> z5JbQ$RYHp|(Eg9Th8v6|PGoVy4_`YgK;;=$pF<#T(!0=3r8QFqo%cy*3x(mVqtB6} zv?iPiV+&P^CCkviQy_9fLtIJ*f-f_4M(vx1!OjeufiQ;IQefz+r4`_xh3*4c$!AdV z)(tacnydWr4Jnb#GaA8{L#E;KRcQ`;68!0e^M0?cSV3CO@1A8JGh@_apz&OKJ|zi* z?c8L-o-^VK_k6;tKrp9PPE)zPY71A^m{STK1 zDVJN{dMv$0-0v2lB^t3zM8iBFXGjhz{9CD_$++E=A-riXviO{83i5|4g9#61Hvm$9 zSj&lW{^Z_oqDzsK;pOME(S1BjmcbbvL;)E}`oB@`Bg4LkNQi zlMzSY%-5jXn*BNe81?P_xwTN!cWFt~2N*}r#^eF8=f4M-Vve1jzXpT=m_p9jcC6>- zw%n)(vhq?npY4@+qBSLMT;1+=91FtpaPQ|3p{%`^&CXh3O+&!V{Ic@Dy#Pr)>5dg| z-WU@u=d+J8u>hNb__gg`tR_Tm2NW}WZuvB1+`+&}7k*6i&p)#Iolk0?5V{}ni_mvJ zAVK$iFbIw5;%oE67_hZe4XinXHhYXEHhSl zW-=2#_7=5?-JrQ$2t&f;|8C~ibK#ZYM*U>@1da6F>N2G_&eJN#zgIuoY$;YzQog-K zFy`5#M<%JLUq7#UQQV~aIZsa+(PNjp{TCUeWmZ3+iAIyI#6IHK^pm?}Bm&%kM#(rV zoc&Eq{c3jvSShX5qKO=$E*VP;-5f^LB?R}WLu9gnAF%DL@!<0pfj@BGLWh;WwhF}0 zrQ~W+B_^IwtbdXblJ>PKlJC$QwE=JzdynmA2i+fY)kN^uPY-H z{Q2Ecqtz0WlJ>kA?MgNZ|-v(wX_*}BkTP&RvY!z4eg-E+@ zCo|dOX?UDZgwO)Z;lcSY`JnSAGD$+ZS?4*N3c5A_ax2{pqB(M(d+29xRuj@1Q!>AW zgqNm)QMLlYuVJupzM&rz8j-kalSW=3nr7n_N;_u&%5UA(5|U|-S6Ckrq!KjxMw>F+KDim>K=j8 zT@{>IJGuS{lw*5+1$4okF(hp3x=*3)c7C4GqUUi6YRqNo|1=vtQ7R~21aDJ-1E5OR z-t)Ta;AF(oN=y&EnfI!&tIgaelSW5+eSiN+wY9|!H+`lj=H%Hv3fi1`A~`=Bc8Ny6 z0zt2GsrP_hlh4182c}mVn~Tk394~{yLP@x@j*=Gzbv6z}FTYK?96`>^^S=PdD?%2h za05<&db+{|KFTeDi|4_C-P4v}e!Cg&k8%bJ^@y*3xtsFk>)2*WGg(X1N~a3dj?%xsPc)GP30M z_vlMzuW+LWaIBqIVTqE`4LV{y3S@?#LEm~XZJdJc+A!2^&E03ElD95ewZ)O0#V<-@ zeV&dceUxZQa(k1UL`wS0jt(G5wH~T#ST>`-iW|7dQ5eR~v-i`t)D3n%+f!muAs2@2 zs<&?djy7lozHVf1(JO@Nce08e!z>s)`kkTp^8^Ra^EVxW4zJ`9Bk>_hSWn-MEz&pd z{^4=Mp1Z=yQo509!PX8ZBz*P9S0bY2O<DG=?Qc1~=%Ugp|PI*-5KYod`yo$e$OK%F(?xJ) zsv|4UYKhCnaG&8;>|x5w98I`$bE^8$@{<>xw{r#@`EHqNuAguUWJe{Ml(#ItSr_ zQ<%Dfyw>v{?74vR&hlRA;rZl49sh`Y36#Fn)J`dslZ2nJ$nOk3({}TG6UzCND(tAv5ab+cO9u%QrXr13Jvo;HLvsba%4rFL#5*my z*%SXxOIFFqc6w;uCY+Yo<>gMdo-J4WQY|o*yK|FLLNjDi_yu~ZbPRdG_x_MPdnKK- z^%!9$Pl1_efs+uWn+Y0@*b6_5PO$RRedxm-dJ?m)ToIXy<{XC3GrDfgU$-Ze-^L!N z(CBK1M~ZKk-vQb(=XKXE;ic@uo9s86cd@4wLMvN)r$5k~dW^?nXwmAvhH*_aL(G-q zTtgYzspgAG$&ieOU~+K@0FwC81Ct7_@!b9MNYBT7{&hIJ7BlKz;mo&){$ z?y|$0Rv%67+abHAKlXU>)oJZo?O?=a~p*E113UYK7CN+N7eVLDa|XqKVIFehKTeWol4C~dt+k2@)( z?_&9d4o~RK%qZTqmOK-HQU?FxU%o}odY|KYf?m|x-=z8S=Um-sXl~=5ohkd@11bByJdn4cj+BJn z6>T<~d5x_R=nve2NsEcCh+GpLB*ri%II_?EGcDPf(8bL0KMBng~6f40rUDP59Qs~3rrCiaEp_qBD=De>T5pw_R0WFcDfJgmRV)#T{@BuyJhn}UdaQ+3JR>-f=v5fEj`s!`AwShG}e z1@=eZ%gD=i$N`|VSCDq^9R0Zs-j7|n3(L;jL_9o~9jM@jb~Cy77EW#>Dlr;$F3ayu z2~TC7z{Tw*vXFchO=K`JpL~c0C*6gf{ck*%G&1Iu|18D2EJZ1qvBDhvhT7!|siBJ=`$im|$h;=&5ALYA8q9@m zX=UoqosMVPAV>ct*F<_jF_%%6o=gkE%I5yYH~Iso=PHOUNf@RM==)ib+%dRH{L1mJ z+WW25ezy>YRvO=`#kSn{d2W@@F#_$^MEA{=SQD*E$DckKr)}zhezv2<6hL3seho7w zM{l?KfS-e%6s_7Mk>O{Ox_%pgTsqMjl9j8aqk%9LGYO5rD`sZ)Qzb2J2IsBYZc?zA zS4WRSZpA&%$jM(#{&2ib+@1Ni==tPvz~TeRBxHges_$A5{0d`9T=i!*TKcbgg8Wpa zB|P?1N9Ffh&MK4Hxc4dFK9Hs|ifIzQx=JB?lI!!hPb@ik#uMX426ao-<+dRjiXq|I z!H_j7G;0o?1OCx7^EO9a4lo%rGY2t>r67YOxyRoEd} z64rl0c`r+yAbz$+OIo)-S8rPuQ5Rg51+GG3Ey}mDnV1(_i{H~@UG_qQyjD+6|D(7@ zX89taxaooPFQ&p_B_ncV!zoUUm4`LDt#7ARl>!yO`dD=$9y@&z|7|&cRIAB$&9So@^ z{H06=Mh^n-&=N86#GZJ)&gfnzHAEI=7-aJC{an7U)+M@X#--bhWR8@FI3$nZ&p&I8 zKJd(^Rv}b7u2!wD-?SBb7TKS_(hdnyZWyN#=kuX$LP7Uocn=tdgyPmH(BJh64*IzC zI)VDT%iBRq(Oxxv1DE8IDPgxzHZ*DHlj$dYov2=+2axtpyLuucnwB^+?O;xHuEbf$bf2mjOA-nI9>;sq)iJgQ&@i`p44Ty`2wFu>o z_Ndm>6~1mO62tV>n4tT9$tpJM=#sD`dzVTe)zfp;AkE~He)XxxcaN`HK0oIr$en%a z9t5a{aiO%1R7*-;f}9F$V2Zi3`{Zi)x39rLL5ZnxtAeZc;B=D(WSX^YJZDyiufD_k z!kQe!7YD1wPVKWR&+rY)N?S9^>%YoTWtuRKob3`*5)*XuJqD_d4d_=C?L(2G)ef#! zmavqjXT)RX)&iQwh*3dky3?QDDeHl)$z9zVIzF5LCr=D-bFH;b^W$GNaCN>TE?T)+ zVaVq)6U(eW<45l4zLW%Hkx{9H_!PxsJZNa=mEp)Zj8G}%kO`K>$#|$7{Je2iZ@D=I z?gYM}1pyy;De0r{aQa1|VY(eH-Dm+hbNvsV8|z>FwjGT*&1(UV>t|Df5C%UbnT)V- zh7|9Ey}3LU3d*N{&jwdbvS?o_^fPP8BEfLVO6vI4C-blA$$A@epxIALHSrBC^4Dsx zigh4^f92wm{`&43+7k#DD!$@PrQnQ^i;$M{2 zcuSWdFSsUs_SwMrnO|eG5B^}$_eW-AF_zmQ6)i7sYY>Z$F@6|3HE3&gJ%R(vQ9xJ@ z2Q3Q+J?)$!XZ5xHoxUD8$NHOLg@OncQOcSM^Xf7Nb`z+gMTWbjFb)F5x3oS+>|Ew* zf#bOpIWB?4Lk7lP z%#W{gLCG>}abdN3NN5TlLNl$N#z^R_bSeqNT-w1W$IfHTe2kC-uc8K3W))*Z;JaP+ z@-R5xU*#z_Lujm(r&2^7dBCh*=w=njDZ-)Gaa7+vr!;43z-x;a>SJQAPe_9-C@4vR zfAXM9*nHu_3xPRdnURmXfPkf;W^@_IStl{Y2HC(+!-qY0noMzc>b38UmS7LTL;2C- zEV6Rt_pj$w4#$<7NBJ<4B?ha51nJ5CLXGc!X)#M_Y?|KaQSgDGcr87I;ca}3v5M&` zUY}6*?u1xWtYl6@fswZ?urAJ=P`q9`P7UgngT;ec6!&mZ*%Qj)M%3b1-2%RU$$K9S zPg@Pdh_1Fbff^gXSrr=lqr7M1Z~NIhkfohhR8Z3N?*bgX92}|c66M56%j_I0XHNN?JCDCak=8%w?YZ&PZFrsi;wBieU{0Aq$cg$9cD9VF{G*z z@SinrZ(2l=rhE?MnpE!r{xq9Qezn)ST6@&Lo$48(dwGqvM}hk{VonmG_LO=2gzia5 z`7(>Zr_QX`a?0xhOa#l@oErYH@SPcwSqGY4v-VWchameDF~qUteAcg+5nLUOcBM$V zAP4%@>_Uvs9TBnazPK<+FUGh1hZ|M$XFuC>cDA!qXN%ttskmHDW_q5cmnzpH)-yMA zT0Jq>fW+8?tP8@-qvkgihvc4_-yHYBC%(6uFzka@qO=zGwYAFv0|F2#Ay3Ifwv95- zs&t;b{M@U2Q|^3nk9`Q;Q8LStlET*0k5J&UUS&)kVj7P?L4 zDHWGd5f6e@VyAk|N3_}!EIXm})faR;wTvLvM9byW(wZ~Nxq5Bc01GJz0+}6y(*8+H zhr_?_nQ!^!lkhrl*=M%Jx|qW>toO%ai~~ayl-y75nMDdCO^4uD=+7ap#vr`6@*9G{ z)6CJ;CX)_Dmda1O)9Qy;H^F+&UzbHi#1?jDe&MHMTw!>`ib(<1LY>ezua}B;wvSH? zqP}I-M~r_DKfkv%u`jZ-u`(vo)YY6hLbHGJE{GV|T#DmxklZ>R^yVF}J>TNnSgBWu zn>RvFKtwj=B4ivM>Qb*U_nhK5vsWHErI@6r35Q~|FA0RqrP{ytO?SJUo*COOGuK1W z>`+(32S7e(GlIOsfu}%Wzx!Vy?DZM$=B%bK5k*Zb5s~ z?;HdwM0Uv6+584xcda5+-6I!OXNc)D9ar0o9@dxhzL@FN%0q%ZFo++l*9!AioMs-=d87hqk+S0a!#+&)>se4Aq!v~ z+t{S2T}%7BwSX&~TYfH97>q&WC)Z;MQyX|?KV6#RUU;#TU3^)v)0b{3y@Vd22MB=t zo;p>ZapA5aZLNi}XNAcyTx0`Rr1YI5y|Qf%^=)V>8J(VmwFfcWP{7 z_i;2qbMC=w)=Nx$8k%Sa-7ux{tkUlMUmzWDs(mZew7jakb0_+OE=75CfMvRX>;PSP zGD4sEsb>EXGW?kz>1P&!?_ne)$)E|h?RJHqlvHEDNoL+9w5$$oju|rcd_Z|kZd5q9 z68i;Ks@n5uw`v;UKJKRxtElzpiW=5x1_qmMe=xA}D9#_SFF}s+r%vLP?CNCHcZBOb z;eOP0Rl`X2Vu1&$~YW}Pi^`M}t8A8ZUV zDiQ{fSAm=wB_pNjn}J6fsCxPi&K+C?-)ib*H>SK^lzZIF{gCX+QPsVDvWp7fsYxlj zGWbJy$4@W%y2!S+&cw?e6M!NftK+8{(suE_0SXw?--#5yC+X_?Klx)c8}hlpN~2%l zb#shwaa=v7&p#+|v}s(Ng_GuK*m87<@Dy9qO)G%b64ef0&^dGYiF~vVEj-NGqa>td zXz%)~<%_c*-BJee3gFRDJ+0txGKQvJv{4y6aUdQdoSON{*F#vX(EgHN4oQQ{sg=M zR1ZT@yVUddVH2Xyy&Zjg!~g=O2Jw5xy}kh_sxI%UJ9y7{;fQeO4ExWUM9+@UpMv># za2&Od*gMh|1zc!o1%7hhy#5##epy^eC6$6ltP7n{F8B(-3H&tGo=@_eYfKIk%ldc> z_?~hAqp?94jnneazIAK9o2!doj1dv&7I$z42<~V?Hg1$}fL!>S4=aRY+OpX!N+tj* z>H}jB7_%&%;T(vs|9f~0B3Sj?yB9I8l;;eUZhGSvRWBSf8xxQ-{Xs6MiKOzSV50(JRlnuwcCUF6nnGRv0n2WC*7hC5z%OwEj^#h> zd!5&0eZCPcQ*!#~Y^e7Sp-!5}WUp{o_TEadl%mQu1=zD-+5{!bH^0XgSh{C-__GeTndDQW@ccPFmvti+ zsa&?{E}5e~f1YgE=8EIrpK?qg9e(=)d}ibYRZw(3&6NmNYcN>(5Z7dPhdO@CXYt{$ zbV|Sc1et5dL6-mZ_oln|Vt+q=KY3cfqqt-?2#O6uQhtkM9<9o)_N!{rYVr!BwdLPM z?!vxzU;jyP1i0XrolnSXJ3BZ#nx%~IeH(P7zeYztMW{k{T1(CQ4aDg?n=&=c(g6Ty ziX)7y;f&)1^Z+p<6!a(l#O-vh4_ii>hlJ3U zDS_K*l5&-gMEZNro=C;UYcQlVlrOiuTSvdxxiBy@-H-`FGq^+tk0c16QG{S(SWRO> z7_xa2-z(!2QgjG)|9c6pT#8hEpNdFnUxB~q992$t{`~v#OgBVb{YnO9{<2%x!oXnH z+sEj`mb%J_OYca3s1v~O9(^Vj+dexf7ty>}0SdjUicS2o1Rz;~|CD2~XcC$byPX(Vxcv@bU6uVi1BxfE>X>dCU- zEWMR-aK&O(bTR()Np52@1S;nlMqM%lBu0=x1BD{E9zuGUXr?!J}7NCW2ggb=4}IJQd-6pPKbw8+}wV|Ckk zW{gD)b5Y?ye^Kqepsway_l=idf}5=U(p?8$J1$|c>bY+a8_-PS;O=qGcJgLpq-OOG zR`Kj0f%M34HWAqxU-`qoVD@ku4!6Pf&Rzm zCV_#NL~y>J;PyDvQFp;HNr|RCCz+5>qf?$7jS!TtpfPu9X!67P@~skyQjClV&(YA3 z(C3c!M#6Se%pQ5^XA=cz@W|7r&wi!CoO?n5!B_k!*Q$5!;)cw^1gs{rxe&dQo`J&!8 z^3LE4ztRcA|QG!e4JY|nW%2b(j(vH`g z`PCKHbIOkY{EDcQL$Ty|m^xL`K-J5awyKp^O&Xr1kR18_N0p3sm3nFOEhz-F`xUxm z2c+wVQKcGp+cc>5G@^c|{g-?#zaiYT^JKWFVbzGyf^kaFaPj~LpVI*Fi+^r7X)50q z2;koRMw5t5_E3H)(^6y^@#ce(Lr84o3;ta)kQ3pb3T9tC#oqgx|2xfsa>@Rm8_+0u zch5ju;~bAg>&IRiGGdIc;N?>3ociL*bx_tx=se3#VW82u)0(2*eZ4y!0z;2h0nwzM zX^PXDQ$r_o^P+OH45K?x{D{fu=23BNzal5y%~2&)c_|WxX`wcFJQM<{dK=Y=M@TRi29>=+hfgR`h4-t0m=5`->aAxx zFjjuU;M?7Z>gP`(wLbveBh+`b+}~IN^uh@s5g~pJ4#gw+k>F~rnJVlL2H++yd@lRm zOH2SKAi346`&mW{BH%6pNcq@F9|va2V7XbmOc6*tn&=pyebGFlwXw5LOWPitbjhZj zm<<*pX4x)cx&OM1xs~x7q3|G7>rGM;y=Q`B18>ULd)$TI)}Y=laOF-69g^m090%oe2~oI zi+FA@aRnc`8j+9VlA$!=ypQjeswKf2&K=d;l{IMuWbe)^T zBTLu00=EifSn*z%>9KtCk{cID1jikFls(X-A+zk%_WpLPrLtJ=BW|U9(RXO&S!28WdHQ$+jBD@zeD)ULg!q@FB_1|Z-OKLN}L0sbG z_?+jX&H_CYJTvpL!v#n6NJxv%}EocrtbJV69|=)<@SKaM@D)O zTzy6Mqn|nN3ieN0BTE-w{BgB(MfM@={cZSU}OXW z66)nk!djUd&D_$@W*azw`w`6MJi+DNOa~})l;A#|l~EmZ&b%7Qmm65P^>3*ng_A*E zkGszDhozLP`n!gPe$lG)6YTu2dY;Z`((^f-iunUrDU_u0MmnooSugP?*tcOTHkvXl z)HfRVV&AnLvEGQi!|#C%dhjIV17N#lY- zXv=}@-3P+;l7SDFHm0e_5C`&}SFe=O7AGC}TGDTRkl0rs+0B+bQ68wpfTVjNP_ z^|NVF>rQHtwymZFsg&>Szd&GJ0JkCN#FV@<=(Uy#4YPvS53o10{QlJLQnc`+r@!8& z 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

4V|7r}VLfss*oNv$FJ_&a6K` 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

    E1}Qh1uJJfc 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 literal 310653 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 z z93YE4w5b)p;3|8@o!O{sM}tz3QK;v&kRyXd>}wv1(`cda{x#P`QFe~vIYqy}YEdUo zJxCFMpx4@up=|U08WWXt)vR<5 zWn!#V&O^&K;;pA&KfL@0clI0+9i-U=IZl90Py|0%V@qE{Q`5T(|9)nK?*N{n3%Kc# z$3`S8t!FOf#9f;O^_!XSa=Xiz&zbvSU-zN%+JLp1q>iWOGa++()}tT)5AJY?NDisO z5sy)gNpUSyke$0j@@ZK7qU?}l^-kX(!^uBFA5EG0j^Yw)Jxz4Cw0yF&C=!=z<-!p9 zH*#MC4v0PA?1=Q1U)?%k9ep~+Wv!S^_S)tN8;6OA{k|4YYhud>A2W#NR|Yf<&&0oc zs;}6h@(QhhCcQ#hIPKEv{s?ujaAno(?txKZJ7!;W4s(S>oVO|s|4w#ZskGoy@ivmt z1jZzUGFMk8_)m$Sp5j>#pQm7~_MOEfb?%SS)wz?iy}sAM=#Gq!qHS7k{`(v77NJ)i zG<7nI+g^hgfAZGUK>)nK&9z zbmp*W`Hv-o{n2Yp^i0IIKpMeKu8`Ds*-3tdt$N_8?;Gyy(X@%d+Oi7LzUQ0j5#+(F zfBLexU(MTt9FU9n0FIuzmKKWw_nm8gEtl=%Svaw3Jgc=lsui#EWKUf!SFPr<;gl=? zq{d~yp!=w@-ZF%~NI2D3%j z?N?eI%5-b0^RE5=b~x1eV061W7CT`NPy`Rz_5O(8-QKI{0dS}o+uEMH@#or@|9bD` z3xT_H2(|=rmR^T;J^u$qI=?}2R}A5$>`8Pav9^5_Pfb%_ZP-R%&|YAl;|IC_VLV42UAoxsRR3{Vi_#%=^`0#FE4;JRKJXbd-7Hs!T&?trUytOG}pmb4EG{2&u%y)8v6Q_I5Utt36LfJA`vk_`*{QrLZE0y zk|*_abwT=VVrjWrp_TxjzW46kA5ERJlwMMd=i&pEBBrIrY+<$v)0j*jM-`dnLubG52J(+M&RU#7yq{qFz=wwZ1g!pYqh;%Mn@{2ZokFysliDfwj2M5r z*R2wdn?Inn<}lM2NW|BuX0MZ3k3>>kk*Xt#KRX}87aLd$)_@mnPHM{DLcbd>zXWQ1 z6QSK1Iv}x`@V1A;w)py9n(P+1mOPbiA-<=p4_{*r>dz6e>9RmhL;1C z`L))i?%9g+^76JRw;)~e5Y|Yno}S+6A5U(+`1#?cOnzMA$jAunOa9mV`HJ+{dp1;1 z9>uk}16OCO=6B&%coiNqVxU2_!5-AnA$_kuJ<-k0EqhH^A!55!e(fl@`9AqGJc-Ku z(ZHK@8*AHNb3+;Df4}*s*kjlBoqepnv2m)bfa&DPsm1DCui`UsjDny6h!u%f?ID`l z*ceB8vDv1#N3^lFmj6_m-_cd$5nRv5P(yb;q5-H2?g9)2(d}lJE5e$9nJbbt289$A z4)OgVOL_GxQlTJ?fAB^OCox=9cpVGHn#0ZGuZaE+r0KAXQ&Lh2Bpj6CYayL29i*R- z`Qo+F_Z8BVX%t?e?xE{F*KrB#0dQ#c`ab&xBQs9r@E`Eq#$d_|OHz|N%OIIV{-;kE z6vugac^g-TPC$!)1`bzIwgOSB0I)2~mdHyK=_5iN05RQz78ad9fKZ^?p+O-7Wh7+^ zzLTHT3amJYPA4LC3h|EPeXQHZKk`@p+tz(Lck-$?{Iw=6ODx9^AMia6NvWO-`_{V znE>U7tQ(?N`8IwCG>5omALa~*PaRndP8&Ukh;ab2zM5NFkhDraFu1_4vxlgDza|h9 zm6-2V|61dJD>*s~stt8&Y7$1|kE7+$K(@qUQ%mzLY2WpK5bg)3>u-=qAzL?DPyo`8 zK$aM=iokpT23}V9O@hdDi^TNT0z75p*&-#VDme#9T$h%CXd}sIE(*KVJsSH!Q`Bn} zEGF8pt9XC^<>^oNGd_o13?m^LfUb+;wU4iG%Fr-Xg1>k@lz`M%3raeRT`*?+i)@TY zh|%kSE5gLy-V2q+i(SYF2nvVufV|URgff2{_J-cTJrk4yY_iC)jimI#O-C8D5r2V> z`~K0tzrf=cv1~~ldrNgvoE^*)Kfq<42Z{&&&P@q}zsMe1b6+e!w-jg$Esf zUHu0f%Mu0uH^{ey?b;r4z>jvGJVL^;#BCLP*4n@l^8xYF0^h3o>~j8w?B~k>s(KA@ z*s6%%-{36igLe!<0H{$(>XopU!zIj>J4*qIKtBk(N@J7HFDUqa`0P6nh)E-%&@t78 zU*0^3rfU}W-f!G@K!V2|ZbtO$UsUd-#3d&u^P1zxX)qKFEH2>rL~J5NufR2O4$PNO z{#e=CK7a*^ytnX_WKa-7y9LVrvVstE;HrN8tzZMC#i{R9tAukEB8#Q*oh>xM#k7 z6ZZHVhf;m&OvI~D7#(bO*ekC)!6=5 z@k{?UjRYGD0y9dI83_umSd#41{;~g(9%sF(<1e;tyn)>S3hK2_CwRYv+AV#Z@U};b)Ss2UQ{Co>DGe%x=lp{QT$T^R} z|5Ba3=fr}Li6LwgNlu2frdQHdSy>r$u%bipdNhrVCrg{F!x*Y&0|!dFG)*L|8eWy@ zp2Iv5OJt*mBEb4yz*grIe0D+{mxeKtG4JnBm8yY4jC$lG9 z;j0*ufa(FWljGmtB_t$vN`XJ6I6nPuwe@AWYD_gr06b_g$6K_~vjul5!FdR0pv%uq zxQ!?Dc(2aN&H&@}URRfyGSSp`M4=+hx`Svopu9sucs&<*ha^G2A}Z7dR_JpuTR~lc zP}=8kl)+uO#rGMUYXMD)@Dg79{}}u8aH!w+4IF+MgRwSa-!r!CA-j4vCd*i}@5=~f zEqkcM7_x_CZLvooAt7Z=B1#Bpp@>LHsZ{E@UZ3xCJje0;{`&RD`>ms5W?uJw-RE`Q z=XqX`p$tG2Y!2HH%MI*y540F3EGOViwxOj^{QOPV+h*bmvhvZ`P^NYaj%g(2Kfb3O zf(7yHHezy#SwcTUb~|6CJ$ZhJ9{c%aN7diX#>2(x)y2ahu^n@5A$K997~XS5g`~&y zQt~NKU-=#5CPUQ#SD&o%PSNLv_Rs}#aj)XPRe_@`d}!6Tud+seCGb<0O49RT_fxtxPZ|B^0rG<-ug-m z!h(4k`T0Yqr>Ey@=Um_{1^&I;{qHBl^q%^*LNNS_o+r>+q3BY+8oeOzNn`{x#J^`& z4#VEI-dwfT38O4a)%}LiIh3cjH({oBdC+tt`YGby!ujjo?8}NwPEAF@sKd54$D>_t z1%56Ck&)>S-@#|RNw5s>QFN?-s<93sI=4gICR%;fb&m|-#tR zSxgcJtlzqReHET#erEVNE-^S>+hqN6@tbhssSo<@n2*KkUH0sX`2w$aFAuId^+}XR zv98RWQhu>T{T?ysWZ|4>3i%E14y~{79n9oIYGe!4_yvzX>Wpd&L9v+HcbJgc7O^}A z1wZ@rGWzCWhrEILxqwV20VILBDh(bNd1zyyf2lA8pa2{jA;-6ju|z~px@BYT^~R4g z3w_HAS8p;RqWZl27U%R|pR>D)>=V~%|7`JKeB#45z;3p6_UgrxQLn--<>hh5p13yg z0o@>gp}CA(Hy&K{aj`yss6YuPTF+LXRVaIZq1!g0z%h_5zu`{j0GqPXsJjsx6H{_S zV_j`oQ^UTdMU4y3LS2vd7JC?o`0Li8*EVR<0Hkc7LpW!HqbF-^(42|2=MCs)-5v2* z4egb|1p&!i{a8~1Z{h0ZY;}t*HqRSrZY{ToRfdGxb$yzuRZ^8qNlYw#* z0$}dlU{)yrZWotXST$%EZdpHH;?^3EbM`wZ1Pnk}y@$D%chQ0c&X*WyofH)mG%SC^ zfJi+6OxLd~7}nRtBuqkAl5?n9*Bg-5W2 z#KMP&XxtK1nGBRNdZMVg;V7F(?h!&JnjPig{|bTp)kS{-1|=6=W%mAxRicE z=vVhVja~Gi8pk{Mi}&E205HaSOeekb&17Pp3l~A&e)$Pk$jz{RVL10u^efkoCMyV!i;5F90gHw=`B0xqnD>q|Mou_wy%FYg&xc%lI$_BtB@v*B1--KO)_hq_! z7IpQMj%rk*v}mZF9HArgyuN z^5gb;9BUkjylg`zN#vHWxy0l8MM~qQ_Tp5gLOR4GBxoeb93+8+AuXPN&7Ml4Z8FIv zAZ0l;1Fu%+jeShNj&SDjd`o+(?m#*`(rnH&blEylV1t)-g_-`7g;X{!xxBg2cUA_o zAiZcM^d|)K`jHu-;-Sj6$dwwIe;3N`OIK|;^lWV9iCLPiaNCjP4eW&*J(BxvbIA;C z3T}OOhx2pir$QgI4PK&gfBN$>@#zVXkFq!+rf=`!t2X$e6JE=$X3d3P`eFQ+Ed1~f zg^;=Jy?(^U_1=d1vG}T-#2%g^A1QCi)Aac@mNgT$y{9zm`tbWC~X&s^GdN_msmz9w3hWxxWv7F7HWukLv0j%|Ov)5)G>^Z-p2V5?X#%{V`4-TE4WtqPZ)+j$p}kC(P^^CsK0CO@G2X~g zxX=cLT4x#XPP1UN1P_8d3^Ow~_kutuU~Vqv=L2ib1256W#l_`+u_c!C>z6HL&w{iR zxXvwL8eu1vg76gfvNQZ0CnpvJ#UrRu6?(AF#^2C#jFo8K0I&eO&|h?59l$X7w9yY0 zy2M>`4|Kk>A@Cr^~{G7};h2Ph+FhBf|3xm)xvtR01 zVp$v=9MG#E2(iywM_V`;q$st%tu4HQvY7AFvSuZ$p3N^|qo}b>6Olcw}Dw)QcH|BJO_nIH<9QU3HuFa&^07+`9&23Z9fNh8PprOV8O@aE3RQpR zMf0nCJjchci;DYWLX4}1I4{!|neX;|Q4mm9)v!8eGSU;PDr&eXyu_RmaN1wmc5?UE z@((Xa75Bp&k|+%Ej}`ngr+PSS=rpG!ih(dE&sN1DygEgvCb=+I>*0%M8N8WhyXxo3 zjAg%U6?@u7nF~c7x&cL@3hX2u}a%42$Dcvxo{egoz7&7(b6QBu%Q6t)0- zeaY(DyDvZCe&QGK_~?b~xPNPN^CqCS|1FUHUoB!0fD+z!j=!q9{9JbTA@T^geA&_R5)EOv2{Uj_DUhIpN(b2am41$% zh>v*gax7~71s~yN=jwIl3YXYncy%aJfPP!}#y&j=PD1mLw>OtiiWy!e2Qu#e4Iq|D z&Mi$?rqGoa)ocUYehtQLsM#H`?WgNqq+0Hwfltt5L0%L4mzY0j6s%5|q$JwULPsAD zjiKwE<2%rV0^8Q6>l@lfPuH=pPOu5pEfSve^tE_jQE4Z`Sj*FS+0b82hd!_{ zVuBQFWs1!SS9n>Bvo@U!%rm&R-{iX;Izdy$wr1Ozef%Gh@DK@Ky$^&(eVS_?|dd2jkmG#_XM9#y*GJoFo>z?Kx*9m^{x90D_dVlGE=9{B=Yw#N$ip0|cLn}%zb!p#hxm8i7=9@HK@29zBXzdwMb zxegu>Nl^oKY5oAxFm1OidpF84LBS z8>Z`Yg6|E;oeAg#0dfF(zki^0m`!EiZrJ47{|8h`&caJiA3O3~rYMo2o`1Ips=IHI?hLy}i= zh?)VxS)VICBcr-9mK1P1aB4WpL%wYNd4mX|lYPOg+V{YJBh3m53UvIXuYhE?@2E45 zXjqQv5~VLZQXP!@@k!Yz*;~A-wN;}`cj*M|aA2ib9)<-51(n9r@~1K&1&Alj(BPAC za}}qkbM>AZ`>nPWT?qSTqD3ESi~_B~$H!+{p(l5N18H2-YExBQ6Mw(kSeknlbV@*F z`WetSUVYZFqIUC%HtfG5(XI5ey{wYPAD}%qA7YurjmIs2VtsR*&h(j4r-OT}@o`B_ z&e+f$jDP4y@zC#AaHPB_O{OQuAJ(6b2k-kOSTyj-UVOM8dJXj6;e!ruNi4fsmKm}9 zc%VD3f3oh+`=8l@qqpw|Qu`Myqq+{{_QYb>ii<*}arvii&FrJI#Tz2o_|zR;hGIo! zk>ocjoF+P7lqhm>SvV4de{JL`JMO-xlKi_Uq1aE|2WPmrdV)B_GTpKHSrQ^=GH%Rg zK7GSn=DkZPQF_wI^BVDZF^l7w2%Zm#Mrq!wDgvE#YR0TG7ItH@cZa8;1xRk_O74pL zA*^XJs;aRdVy!vCc!QdWd)!3P(W4(Sw(^;gu>CpS`@4{*?@+I#U=Ap&NDS=X-uigwM1#HpJ((Vrt{jpz-Vl+Hi(Y*|F`|aBN)x#~tMfUs zgE=@#F@b6LBXiM37OZwgi5VSuvTLTc;HJ!8{;}G;Q$deUNnRBJ(4nBBQXmsAT%R>` zK*K+O(dMU9-Y?cR&VJGWNOH7>UbFpf(<*lh-cU4zwgKf+`qq1+%i1~q#s0@P)uY~) zC>7k~`0`~nkzT~Cy&E{ro}h!I!7u$9kAwvA7&XSh=w!yA{z6lk{-ZyP^MYDB@S-7T?c$+@P> zc}_C>e+1Kq#7Sm@8a{9O*+m{%MAkX&moHU}|?zyHnBYEAJ?XTh1S$ zRA0#J4-vO*^DcT3zi>R_hXu|W$?E-@dKJqK88fIU*ooivB}&iAD>z0lD*x2uB^-TQzj}EXNfX& z-*e}A&B(P)5wXr$l%z#(46O*Kbl*-9@wwX}EX zEbxf5RlfE)ec>=#c<;^B9WUZPduB?;F=5?=zI=>Y_vqtMKWazc*PifXNCcT-eQTy6 ztjQr&I(k%HD1f(Gr@rCv-#kwbxC-V@`>2$1*47eENAxxx<;lUx;dbi?6Jh~$6ng0k zIS?)`#htr(Oa6(;DWtCoX9yTcrf{`M=$T!mkltK0!%OaeLb>@|o2BneGVSp(V+2Fz zks#r7?8b_f`g@oz@JG8_k5$WJl*`RIQ;F{8{?9)2OO&L!o8gqpZAe^(mDwpY#&Sf~ zw&X&1nf=$;>)ePzIR9=@3HYX`vm_X*l65|jZoT~_jA!ZHlA&vHvm7`;;jjDiR4K3i ztg4GNCV|0~qFla@#4uoNiFhXzJXLD6q2-s!Xqc~LmLYw*e54!+fqeGWD8e<*bv7$` z774@eO>gdzP6w&<<^C*CFf==YV|{s`IyD!MxEXTXYnL9DOEcuW{i4I%&k^m|8t`d? z52_*3WRlYuixI%$mhw9(a33!mW00GYOJ2Qqpj;EfYQf2h8%sbW7ieLWY#KcWD_s8JARwT(*Cdow3uHdpF*sx3(0bYVHW$6+@vZ0$| zl`Xew!uIV?YZWywyzFJmXoU(5$^UeOj7Sii?M2n3I@j1HCV|(Rxi93r$!sU&TrIu_}8=^5qJQ zH;hDPtzuY~_O2ybX=g`Q!g@{jDd;>^8|~dH$X)j}S3MYxjE2z?)c&q=(of zTqX0h0h)&AF?viA&VYcP-uiK^`btH+&k$*>EQPi+R{<-5{v&uw!l zXtW-Aox?tGxabv)GHy8JA7ABPaVz&wpRpBQBtw6$GJ;@$Gql}nQA;UDx=GvxFRf=R zSj&};GR$9$K`tg%!|iQLs!Vm*BQ)(9C|fWgSWIuhol|F57YYn_P3PNN`*XcDs;Oy- zv5~%_DrEx}7;J?MnVbAck*(U0tYN!xKIRuEiNxTJ&xykur0(G+x(~V1slWZIf~&IY z$*BXzf$TTg%Gb?tQFuL~E!qD!HwPR2*VRYl+tp3+7r!5MAO;(fS9tUAFIF6gt+`kh zpHtlACZ^lT?@e(`7lrY$G~OJ{3ra}UCa3$5q=&>^qv%{bHWy*w?HH?{zSnm!D>T4Z zo||FF6i1M>N#d5o4)PqW&>`cB2wOd6lF4XQ$MjF8r%R0&)dgsXYinkylP84RRk#!7 z9?bmr>Y1wTPbt2f6k-xPW~&mKw;qvzBue@0eY)zjoPUxrLr;RgXAvS2;Q%Yys7RzD zu?Wvr7{HfB!CfYiQ*~L(IXiL)qG`L;Q(0^~OWlJ)#CZ?K`u4`2% zS>x5i_b?_)GSRW`uSnIOC1SZ5R%ng4oM~aaHFbN^C65{|yRR_Yy`y1*pIrKPDedks z$GK5A(=yQnM#P#+|Mr=pIB^H-8NSPw!E=8B|D(esAdh7SNa*#sJLpZV{jyweuadS6 z?>MJ6k+r^vkR{|!y&P(tzP>|V+Dflvx!6NUmC#+w5=}J1+L9zlbO}1%P!h{WA+R#E z<~|Zi1t+U)4o-qgU8MM+$QM?nMZNH=J={zOsR+sXyTX^a zna`d#39NX6Lh>gbx7QcR5?FX_Hj?;vtSuC7=b9QEJD)tdeP0H%BB~bmYNdsGvTO?P3Qfx}T)iUIMB;Yd>N_X#Iw3_6Ps!uUdwCE4i~*0eJ$rb8Qm(ra=8zqX zJtEbTJeX*WCt}z&@JzV+E(!xO=`~-5FoYb+;ngY&*l&v@a863SN|-4+OtqP7v6R+Q z$11QYu@obc$P#P1_0O^dE?YRcF|>^XO>_zJ6@gzi2%Ay*FDyUn%~+tVE6Rh?Qs0LY z(pJ*mQlu*)R5UKvK_z{qtJG269h#qZQ>@=q?hsu18(w6K7UfH1Zz2-f}HkFJ$Yv8Rpb8_bW<`n znGi>0NSu3fnZ^4SkT@5qo>zHSO0_~ z%jQ#aQyko~L{}tR8@7g=n!4@)D%)9L%k-mGrRk&rQ0suTT~Rp8>@3fe6I*GGF0S>u zTy(JZl~nuIy#a3;HLj%UsN8hFeTBUWQ!7;~)kXfm#mHueOeJ*26TAdc*AxZZ4@*l5 z@~PyyYCZIOHlJqGTSSt^_JnJ)_2l}J{0;TO@hN2txsthZOSaPFrxB+s5Iuv{?NfMy z4vzgq;tQFfGpyx|wZCt9^uLUY__LqT>9r3A68)Tj^!L3S z!W~k;?mKQ6NA+{xH!8)O!~M9uW@#19-cWZ=2Ash|zwD7Jdd^*rohw!G$5J{4YS$Vj z?F4EWoAqm@NC+EQ%s`BnS4bvAzhFh!T&9I@-Q3lQ*Tg5~xPRc0ivKHB3I0m9+ZYvKueJEr!#dTPYyDS^GnY?Pm##xn`t@Afw5U2Wval{WcmtS@!SNzb% zpDOsn{Bh#DPFtrTji>O^y}zE$gD3K`3+ef`q`3)JY?OCaTrVl zr74^&deByfB!=wOLvSRvNrTlg0rg-5ypBNH^IP8=GD03|N*R~7J$S~Lp=N9!O^2$c zK;Ja4ypBJ9bXc08d&90zZO=oA;ghB^0oc7Wh_b16tS71 z=?^DeZIM!rIhoF(G&4CT98*Y@y83$ljf<)PfQ+V-|cp(@%PA4DnSzGEzJ?tuNeG>o1V#lSYOB0 zD>wDBSumdrM0YAf^ku#%GDW=@?2n458URR;<1AW>t#2?IF+ zU+xf)d|BY{l;MKNi^^5r|Epflew!^yCUfw!DHE-6pJ|c+uaItQW|Vw9^7QDIm?VLM z#UKRAX)%o4E+x2q5)+*b{toPmmF$J=cW5f+?-8%RZ*{B`E{2Of3=^F?$W|)GE#&<- zL%mOs*i9vbynNuX4CPHCnq z*|g7S1LKfv4P|H#cS?R?VNglUTAUL91(AGP*?r-Byjpht&+nh98!=Kk-nQoa{JxLN ze!?x~A2&Wfr}fL@y?MNsVyS9nkADDNI~0N$`0inaL$=(lL@h6vi~Emc6Ezsda3+zTM}7yZMChE zK*Le>p*39hZzkY-WDO3#*PD<{NWj)|PJTbo^zjl4e+w2zGA>dslERfWwnVR+bK1e4 zTh}YNz2;nCMqxk<6yR7)@qfJ`o+hSgd7*9 z{Y^#8E2pR($@`lY$vplNLih69 z?~m<15EH@kVBE<6fgAmATdx+Y*e+Hv!9_9EfW@B+g&wZERAhX5A2=LNSFi=p+4LZQ zXY%l~FH%ge%J(((?9M6wLLsiYNS~0jB}wJdG@hkq(bN7I=l;0ib=_Hcau<)GmV#XH zP06@zZn8si)9mGAf=Zse3YbQKV<#p=EOffxSN&uCW=8Q3bL|`P7qvf( zJ62OjxFMtC7z*xJx6$epzu|U8M@WZxaUv5h?IdRF8krN6IWtP>Cu*7_x-oAA?awOE@AK*RRUaZ|%jCJW7@h3CN(?><3D*(nAe~x9{SR^nzZbdUwwE^>#uh_@@^Ph+RMp$ zGe%z!cPLe)03R2~JA@l8#IGjORr{kDdXRnYhuM=D*d&CG59zJAINkBc;+DpSH0oY5 zV=eEm<4x1KG;B=5>2bbvKgQji+fMN~fiZNKjxUdVi^C%{yg(fZnZysf?U5uL;MPnx zuPCvSQcK#QrA4uI>!BdD=CquQO_DV^p*7Ghe{|+nd-2aOM!s4`9QGlB!O4EF?aCE; z4xL#e()4(l6oR7>e6P@-E@hi`7&TccnTLYQ9X2+%W-k&Dqkl5RbzI@ogLXuM`1m|8QzfHh`^%PK z<3j8X^KlAUFr9Y*3TNDSuYr(!D#Dq_9m*v8_UKyMgAe(9X~Maw<)y-G_W88Fy$gv< z!vkkp?6f(O_%0gex**p1slv>MjHvP-e@tq~d{2j=3N1a+`f;LlBRL zZT0ouMI~)PXV@b>4x*z&SoYO61<6L05Ikz41PTWLwLE-#RLdub;?5wPFp{f79wXJV zLosHb+|!i0mg_R0jFP6PIvTdn)ETGp$L|_GQvd_%tTkFO@w)3%|Pf)G>KI z&zK7D7M`CGPc)+cklr=KP%YYzOzvEi7icnQboRj(tQ|HNmDa+Sm{9$8W+{#<{Hx*V zd3-6-P0f8nSZu*YjZDD}{YKfs=VIj@Gy+Yohrr8(r)8HlZs3$guTB#Rb1+zb3Wh!H zBx}_OYuFMmle~##0(%)TV5(D&TeCv#M!R%PK&D!aak58X-651{GNY9@Bxqchhl)Jmn;yR zF~#un?P9P6UAO!qsoogHy|$BwyC2l-YwE)~>e%v1G+DRoEM?WJ(sBqfR6oPip5Y9b z1jZ)Isx*C`E47EosC&X+MQ;(Ov`Sk}Tq2XGHUcC>(psQS${H_8V(d%M!ElGQSkC3y zs|Oi>qfAmuTo%+hA?p9+Z0zt+5^yO6P3zKhOd^7Nyv?jy zSkWwLo8UEYQaSi-qv*8Ov2Rn6%HrqSUm^)H`zt;O%ki9Knh6sNq=(FbDj>tSQd$3t z*%hJcjj_WUpAJ(yIM1;7MrvsazG993YzPXlqMDtqXyxksPi9GFC8y+quP(EFSS3()8sH2XBgw?Y9NK{d3seqE9^u`&+y5GtvgBQCGMx` zvudaR%IdrEv`9Zic`avj-JOi{;U!Tj{oV^H-(WGEahUFKfXX5M!=az@h5bo6t=!>f zrOIM*W@zMw$yQ2kg@NZ(&FXZ2jm66}=@-I==R${Y*{VZ5fk&%mo;&5JlKZ^LtdIHP z7hW)wtuh5`i6ALsZ_`qxBoX{!LypU=3~+kS803x~g{vM7;fv-g+c#Fc&4MljeY_N5 z_Cd=f{S7ebGm03lbBdmczYo$f8K0TBI@Xe~t9xD5Fa)t|aSnm3ht8ly++$nktHf`2 zCmkqAh;Y~G{u=V z@Z_E=d~Yl(m;cESpX$W&%A1Z5{bxIuR&1s7ZgF5GuymthBVSAKd~=gCtVh-?j>h)k zk219EXC;nEj<6Mq zyL&qf?L6yU9Jarftj0(l(f>2TckphX-Sb<(-y_!4lxvDufU!iz+Y-Lk*!)Vb0^#}P z)%PZB7?S+3vEgv8*_19abW7W;Sh;Iuo(+eqhl>J4Hj0sz$QZ0-5v*>DkWzxKgj7)B z^m-t5AG$2PNxj^J9=Hk6IHM_%_T~&B`4mBhe0{Pm)|K<6-pdFB?z)Oms*Z#7+5Iq*#?neDu=v#W&JY;Ul zBe6$L=@0&Ryol?AGHDD#V$~59WHRdw@AZ|~t%vPv_-t_PNcgH;>VtPtQMb(`D$AO- zp6f1W-#v4Jf?MtbeM+|t(i zJ#oi|nct_vv-gyys)C$ckJjiL!t0HNR8YqDe=4woEWfx4;uj!LAA;t0r zHuo^ih+@h4y_HwV!-=T@p(^=tUIq+p$<*1K)$0aZkE~*tjDG4}|IY^CcTG~JsG9_@ z{3PRy)k&r#)ddghDP^_|z-@W`w%F%KALoy&-Zl&a4rG8Q7v7VqCKlkYG`})P@UQBh zL<4(JCEVG~M|+xuBV{k>f*AxQpJO)-ie-`$T|tsRt| zEY%!;9dI2V2(<89H7W6ZZ^I5vtT?# z2zo%!OT#oZ_T@imd#>orCooow*S^z9d~E&wzQvj55I3BG{KbF6#>MT1I09L9kfR?@ zb}rzvYOBcC%a;0a5K^!ohDmEm@m$7_t8$ptNr%ylCp1`zz2^c;`2Q&ARLxJ6Mx4?# zIB^DKXXqFKGzS4BSm5(K4G##_HG{~e^hC&Om{y;g9ER1LwX2b1(-F zqYLa7n=g~BaBY5%4tCrSKM;o`G_Eo2>0fx8Fq-?56|0gUWsNu`JenUnD9_L-@n zrSgL56H28%HO|j7c$G3rfR_mndctTv60Y3OWN>IYZYxAUZOdML?wZ%RPxt0@+suW^ zhaY+@6o!SK%Q^Gg(f?ZCS2Lm^m0NkaWnrDapT5$`f7tL@!LhfEYcCw1Ss1W{k1_J= zue65Vyv|YIaM`c8CKiV1-9t6FV2gnn;{O}J4Q_+#YPc<|Z*6Ub@dO|#EXy}j7}gjC zXLb0}Fqk?0OW@L%j?bllH3P)M-b=$5$NeW&ImTk8xeW$x-v1RiTA~Txpm$1=4uX3S zv4h_k1}h9U*Msty{#2u(w$>js@gM_gc&B6gtTw=Pzqr@k=yl+bP2?|Tp&@*zn9J(g z*Mny+XnReZ$eDZwBmYpn8^|2cVg)`DPXU35Y*odi)t62j<$8UAj;OX=kS} za{S)b^FmNpfthJPXxW!XOHJfGz}x}qQ~B!ug$IB&7){gzrPVcyzBuqMf&sv+n7;M@ zHAMk)m!Y}5 z2_z&?a+*%INP8(LDpsdnU!qGJS{}P)D9x|!6Sl7))n6~ELH{l&(BSU$LX5A;BP;W3 z6L1d9HCdN#_RvVZ%blOTeV8!G7Tt!JiIL1}pa`U*vLG;1z&e7@@Xf<_U%)8)_fGfD zI=FejgxLZs4E&YTX@+lMHR(WfE^255ueufFe!)@!Ip9d)-reBU1XG|%rr;NFFgi?c zZHVpdXhN$a_yV5&G94wv2`@dEl8(h!8>svSsORb?ML^&}%wa%Umk?x%Y_lmv3?W3b5pe={& zRv5>Xq0+emn%Z}luG!W_jOiYmZT3S#;VpnKWeKiOPkQ_SPhbZ~<~1zAkwYE;WzJmN zzT(FqHiP9EWY`6fN8rG`cX~VAunT5!K^Dnz1a+tXxAV@(9n_dsz9TE}6v zhoTcVy}Hp<61Z#!D=|;p_m8oVKH%u+X!fbVwq*1uke9O{le=~J>VN16BY+mD2MATV zqJkYTxoW|i0p;uuNYMBVJLogWf0)?$yWnSo$wO}`-;Q6&xd{ZalarHGw(^6{KPS*Q zKe!kE4o(K}hrBe~=~yhK<6$Dw3Ru}sf&tAP%#I+4g>W42%pFL*5e9Q87}romu(OgZ zQzkHiD8z(4gp|QBOi;PLgWM(*c7la$8>VosOyAtSd>ZDYp~C!MTWdc+l?$%D_mG$> z3O-VJiKjqG02`emZ3|LW@U)>Wd~m_f!Ye?1)Zie)BWQg@6|^Aqge?wwGEj}ifMy-= zjrf?263n0q#r7E-+vrt@dB@`Kgmq8Iu>SwEUOEiCIxPlPvpRdozEJwd2~LCXPFiXw zL) zl32VFY$j&5q#|~>{O1i*+`ep3OcmU4QIVlz5|$_n;xC}N#fOTHhSfUpy!x^w!9`4@ zKmGpe+zXNzVD#WBEi+c)ZKm5c*1jayi+cpCKeWDTJ7fBAtl^pbh(?s` zO9Sx9+t}o~KW2YJF7Sf`v#hDPCIIq6rlU+fU|u^m@zARn<)2*T97tKa((8^>rT&jw z)ve~Zs@s-q&9HA>_b4K@r0cBL?fM9E))yen@KC(<*k91sw)mo%3*_KREX26NY*i0%ZtAux`S~S>AKR;^!q^jXj8Id;l!N9T}EA=4|8`uTWY^*1p_3!C2&RTphK0Q9~ZYgdURK^N$bpVX-x+hL}2ZJeI86$DQeM> z_aJ@2bZi>}-SxrvNFG9cSv{QRmtKNjTGc+RP-sbYdOpz)n)o;l5 zEM`<6=J+|5=hx<2w(#Ds^$o(DODp+MA0eCts}{2Rpell}5#;08CzXwB;^0s9ir=no zT+6%#`slx(PIi|bJo_JU5iO$dbU^Ag3&9Ye{J!UO(^J{hp%EV^D?t)MR+uAK?2peK z-%r>Mc#!hEA^k|$#8OvWRCNa2UuI1j*CpdKtvOSXGVx_HK8L%vOP)>dKm9`y*n$y3 zAh$5W__sXHc)W8a^Oi~~k`n%D;q#;N$JG{AHttUZ*(3Y5$k+mY$tK*RAJ?TB5|`l1 zOO^y@m+7yHx5QT4JqK@loN%r9jpdIJd-GJG)V2^OaOHd%6ah;xm|Vf-ab_V|K+p$0s{oqto|~dcC7;e$ zl+QT%!PDC~Qt@5uXXld9PGKLFWs&uS(+&laCtD;KKD@VKZbyn$GM}5|)G!tH~Cw5lMA!z;|nbx*%+nP@H=b=s0Rf3<8^EG}I9|{pL_20Iar1J4~1U`voe&(%!SVnEnubGyl8u(tp4v9^s)~ zI+_hRBY<=7o%-QAp`{SyrWf&2upZ=-+!+^84QltJ1S z@m`|h&o_X`pReC&419C{UdWu=X}6(|P5b&Rlo60bYLJ78ml+%q<^?h}gUB(j>q8mc0Es zI>{K*lUfx?Qc!Qo9o&eH(Y>dd$XC%@U3$g+j>$%(D#n`hNoxO^H!ZX5Qbo+UgVr6cUiGB@Srg5OK=SAT=|bEB?q8<0OZxszcb z9ESv-Grzv&iVGp3P^eKdXgilSv`!`_A*tl?@>?5(Y_2U< zB_(?~5xB}Hd>qc;nm2;`i%`~Q)5c8}&W#TvKfm6A5;t3Q`P&%H_xg9h;tmdujHKr(mep%Y z`V)qqHG4xIG5k;FegE_}I+`8AMZuS4g}VirL-43aKCMsRy!v=tzV-zKJ-pA=Eb!Zb zt&5t({Nhik7v~o$$9&JLzfcp9{q7QgbBMF+f;9w@RcGLER`$C8GU}C~WEgacXf1_w zNi;kVcI0`Tw>aX!paDaA8CM+%T@xJZY~nncmUBMm51@*7bCr$(jZydj;f)-FD1Nc!m#Q4w z^K!f71tp~SDL=|6Gv5VNCg_~duqo6+3LGOaEpb3!pb;|7>QX*R;^)r-^tZ)rqp$kk z-_QThurPQ)aL~h6y9bASo3|jc0iCK_d~!*Aa5Y%3#(J4kg+hOvQtjB|e*54bYbKF> zO;o+p_r)7yIW0IvxoC{bWmghQ4^zRc=mB=}&%kzyApa{#VCnN!FQ!P-GV&YZk)y zF(+P{34pY^Z6Rdvv)bnggcQ@vimMN0P1taHu`?Jqhe-T6cG~t|z`@WqPW7fm{j`6} zRe#%FD!&)r*>u@D-mD;d-n)W~|YCgZg#?l?nJ{Q|_ozc>GO z-k?S&N5^P100RaSqsGRE2mcZ{ex`U2HE|*41xmWm)kb-o6kSGBr~owqlMfj&%V1~o z{`+Se3{#xv7`HQt|FVoLKSa82#j@8@s^tN}S|JGX)=g~1)XVKL+*#rfm}5B$E!Un!a@(N6V_yQH+J^0ZFKH8{(KmQiy&Dz{Q(G$_c>UH}vo8b6((^ zo_3G0jEdTVNe0>h&#K__b#!tXF(1;^wv2%F0`>C4tGhqoPp8{L=FlJ@@EOu`BK6SV zBu`+VE`5Fb_J65Is2vjaJa}lqNonKYATz7c!HaIX7j4Q1U#W6TXlQR&zwlL=LK%1O ze&*SI@SuJELNG%oY)Q1yfUF3#;Rb36&3gpIZ?7Qpp<`=pdd~&Zj&l~WE%2}A+93GF z%f==QmW?}jxgn6%_+hpfRGJ;wSL?9-Qgy-?hfv2Yl){UbFTp>Xtq&-H{A83rf?fCx zT(1BwYAPz;qI?7pNuX3jt=nj*6GTZBbX+dgu|tSA@N5#;c&lFGkN&ql^9xeFgpCKx zIvbwOHk@jVqFgGp{B;D+madT~8^o-JfNOg%oSY|3Z~TBrn+UW}afAwX75K!(o92|H6$Q&Qg8D zj}cY*_t}03)(d&SlN%lX|Fk(uVqo>QlCH3OJ(%Ci~O6p^cYA_TgYsOT9u9osT^o(<(O7?M}#|{pQRL13=hk+Fq%Y)>2#Uv zh1i+dJIFK5oZgv-?yY<+4m`C>JfW~IQEw^p;eY7uJPNiT=~N83BE%X(4jmk(;K6S@ zLCof9D71@7&A#{5iyuRuB5}119N2J92&M|~Qm<*6!MWF35yFb-aQ%CqF;%b>D12M# z$K1{5Gg`;y`DF9Q6HD#Qa7(I6l}XgsPao#uzDb8~aktYby=jVq)YKmG5r2ym4uoW| zbW^luR=6cQSCGpEwxj%z9d$)Yw zc<-35^s~%qiZYe|Cy|lynHjsJQA&`!fDjRrSx==?2R}S*iySDrQdh;7wvuEVc`-vY zFW7!9n{7@c{KFA&cmtlQAxUqE%`%4VuGSDgzsyq8^G@Df{z=p;IQ2avMm3R87_$R& z&$^+E{w<_PD~+%@x~mskDDfv<-p{D`@)W0474@@e#dU+-?*$0&FFh$zV?@CD3RYoV zYEOc^T@7VITGzqi+C;wXY+`!P3)lSoe8CjrwU-9tCS(fG)H&~XG9JAB(jHI(nIK#6 z8LOWDxdDCWM`)zmLiP=!CSZ1zcIwBOTa9bk!kVizc9pXH*JuCjK%U7a@PPz0s}8;n zm%WgCvb$^7i`zkWfIJw5s|GlM;HCi>;q%^tXbK$QEj_SXZ*FcvrW@qZKoSpZMl^i~ zQd2Z7TVS8olpa5lDZ`xx_qBAi8$LE#B6IKzWI>_ZJm=HoKoXh9YrokCAj>+SmY6dW zYt^P9`VoHOI$EZ{dk+W46P$Evd>?UdlU{x+mbIu&J>Os#9C=GP{i*Ii%VWwjEX_%C%llD2Gte2w6ZDbkLPNa)ShPL`zKqLmn(fh>C9N%DFvHc-j{f28psNA1TfO_!j_RWf5+=wq^g!SdL=*7TzD7grpbzWvYib?+e6L5g7Q#NXUq#wYh{^px zfdLe7>Tte1P`tkY5ieam3)`f@^>*Oja|)On8x2ZuOSTR{3+fz|H8sZH?`m8H=PeLI zanjrn4*=u}u*Le&_CX3OI{6xqTBF;YMtIJhjnhC2J%66P``}-or@T7CfinSaia-Yb znB)JM4&awp*!8MnJA~{c$v7k{dn z5awa6a!!qIeE{J%ik{R?zoQ0P1uMrv=m|uV9_NOrt~J>!S!!|Hk#6Z>F{tHDtEI(| z<+~1#0=XXv>XGV>@Q+=6@n!1UfY{7ym0P|;JDA9uj4w{Fd=mFgJIb=~O?LdYIHmc@ zk>p=e!VA}q2#OF-+l-Bj{Oh9pmnZoVVBZ5+*2KfuqZTknv16di4QmJDtv56q~G$xhz51t74e+qujM#K@} zDz5J;Bd>aAn|r}lYIotT!`{dvoWt5py#0QRCGg>5q80p#RtBcTPMIqN==rK1Y4}@e zHvrgmZV`n|>WHT#pTJ1;O0 zymLJsZhZiZhgur{r?$#VmN$9SNjT^ZR92FLrN|@T079Hj<7awbDPW1s__t>)FFp6N z9<1cwST3OE!uvFXvW(X9eB&cuW?wr{POJ9L{7(JX;Qra5*uit-MLEGz>JPW?xN~2p z=zIK+w`WR)9@ABnonwYZZ$4MwTup7+xQY_qegE>YsAzo1sQfC1Hf`<5^g0un-rZ*1 zG!HRFjjbVj?XUOwDR_Fend~+<0p^RX!RWkLmaVQRm4<-` zKk0~RX@W3cB}PP3l(DR^CK`p({H%cs zi{ldq_fY?{cvMQFGh_Me#@ zeUN4Xg{P}OL)c$}sPW^8i3gD0LX8rKL$t~uMopZL<2A;Rl3^7EW)-PiOC6}CQDXxRV=ck53V2cF4h&-{>CI9F(M_VHGwi^2w&}Ev zst0>QHBUZs)I*W^;!)$m?jr+;CL;*3s0_YJW09H_jAgwC4@h%Z9q&B@FA2a2ZN zK*pP>Exu2F&w6IW(PYJM_5Q2E3x7e)uCLkN$_g2cr}C&5219!E8N{f*Ci15RIDRXQ zqbpRu@>OW(Y|?-ai^H;C$_VANxV`r5F$%%2+gAkI_`Rz)&bYfVrDXnIb-oyLH8kA% zqq-Aq=qlZL9rEke@8^p^ZKUph>J-#vKQJ-B{dc>LicJh`pB!V$_SKnL(YVR}*Yf*C zKttWWeu??F{*g@OfAdNOtUU7@ui~5yj)fl{9aDBv?EdFW_$0^cIk3_k;Czg6-dnDq z5q_+ws9B6lR$V2MutC!h8L5J1GYDDz0Op~nnAlHn_W(U_x!_ku<2q4*oP%r!aljzc zNQ*!QNnkh@34p6xkmW!#BMij2K zieNO|H$NO9Q)V8z$izYEbDOEqon-#6Rf=}| z^{id#nD8voTi8DLyWPc@Rz4Kto{OR)RXAtwZ!}&0K1)W4`a)TVi;0iNO61AaJ!aBV z2_Ll~eaEnv$B@M^X96Jal(`IWoEkO`Z^T2JrZSx+6*WN4@YgQ!5Sg4j&z_?s#BZHO zc({@p&YGHNAW-2@y}FkUW|w5zEgtW$Hyrw_i_3W@i>CGvlLtU^J!)2m)kBVbUSW9J z78>x-G|Lkg7Y7lnSP7~{N7`x<$n`=FVGRiREpos8VhI6a!iz1m{Ty*6pT|8bwS8St!88Le)Ut1!CUee5MDJ})`cUhw zF`TBLbCP{sz> zPlSh^={QF3Cnh)Gb0+o^I;Xg=H%MHLXW`dN(XE)PvYs~B9?^bfuWw%MvF+qnv(8Tf z0pQp>3mG5ms!a^Hz6RWZsga8zuTBP{TM9#l?MugN$7Tq8El!KSBBm zZ0@(|p3_Dk-$#OhhggmQxu6-+P62_dqgSE%&Pt)f7+Ww7p9+E9LiAy$ANr|D?)Agd z=Cx|E+^gQ_t&WevL~+ZTQm2XJRCtXT+?f0oVo&1z0*uwY!ztmG{foO(_EZcuU(`*g zjGV__;cTX=$_tBAoi`s4&@(%+yn!mcY1dS+fj}e+=9VHA7K->D#-4>y5$HpkLrDwG zx%~f3UI*v>pkH^_W1_P4f`%D;LLew|gYDgzpwi9;U0?Cpm<hiRi*Moz0wXD zQI7y2Q~^|>7G?Qe~si-C?wY2Z^UkkTu9w-HP_4f zM^&CWAE7MUInYEPt8J*tI2$Wd>MByYrwYRWGY&Fz?oP=5LcR6Bw5KV4g%OXW znrIxiidl2C7r7fyt3#qzbl(T-p&jQU>l|fE_@O~yJ}PGPCgKe1OuccdyCnDZZ?Biv zANS7Yy99SN3%Tir@V!4jLDV2bo26yzEvn4tbN%d|BxY(6ru4DF83c>gCs#tWQ7X#( zC=C`5zMue=&>mqvd@M;svGABEo1at+bJTMwHcKoPm@IpH}3}^Dcf?(c*Ni z@*V2QrUbeh8qu}`5~tkV!+H5C*-dcD?+L>q-kg`nIaCj3yY6}K7qlOnkuzuuWP`S**2@o@d{I{)WYuJ+|3 znIa!Wrf>aGwaI0=Sv3Ca&ClEQ{AO|H(S;VhqjMrU=w?xkXNu!FoHqwz6QT|W@#JLW zrKjY_{f=N?s=QT=<%7_j89gwUpXZNnF)R{jDP=%YE5;4Do{Z7af>HUTM(x32ia$jR z`Q+_#uPFZ36HnZ52$w^Hk5MoqDYF}wKDAP!+OaQR>LvyV)%7|<@h{J-Tl^k;51ORU zP2_O5*b%lNe`vo@{;m1)lk5%y%+FqUyDPbz$lIO#xp=%af3v&%Z3uVmn{RvO4_`;znidB=6Ay{Sibzro8`fj*I}b?`|XQf z&gJ)7Bf1EPCO`m^S64RKYnJl)}?6(Ef=m?e6tCnwZ*!Xw2l_dxl1SoZ-H zJ3l6x_pgTVFzFG`aUSlm&I}w&3cek5CX98HoUQeOgls|lWPAx2izxF`Wd*OS68iNt zDOra)dkzDLH*!Kz1CO7j?GVuAyxJblq9P*E1O%Or$J>3k$i=y`aS&c7c;**+`y|xK zqU~~JiTMlEj~_zJ!v&F7?qATLQt;Mirjv$%+Ash};y>GKh^O zCQ~bNtX?p`rmacoch>BA>jm-onUJ&pJ`W!|`NK;`!L{2Vvdgmqr^oPIB)E-(0gF#; zxFRJigq`R2%0I{9z^2WneX@|6o3yE(cGf;zJ13#`N^0}wo}5F{#_Efzkc^4m^7y3{ zQAHhR&6U>sC~rXwWwpr22Xaxa#DAW~9ECVuI^6Qa0KxW727t>hM&Os?yliPsw4k+^ zDyGTE^lsz^cF?e5LBxu_6VOM zuJlI+IvV8GOrPi+J=2jI&CzlEoHd)6StJyO4+Tu|4PP@5U>$JpUTtbPf~Py>zdIXF)30W#{?~?Tsr6+ST2+ z{k3%8zl7c{hLz6=Vuk#a_sYhz>bqE^VvQQOyyN-M z;>suj@hE?!ztQ(y%NE)GlH0z_Z)s7n8Zo@6Bjp#2=4%h+hGz&2C5*h;Y}KO*k2TD! z5xLK}9zXM*0p9Bp{N+q2t@FGyGwcgbh1n?yVxrrpkY_KOH_&HA#clI8n*$s(I=IsGFwtdky6HzuHD2|-m(zHq9?Kt> zkh^R2QXoAaHQEbB9CKu*Pp93{#~?RzR2ieV7+$@1+@f3`umGx?ZY4hZm(vNHMobaESc1;I*QhV$N6l4;*Pi}VGh{=B z(#48b)4=Pd6Sqjkm>GDmT=~2Z>gloe_Ug0HgZ0r?m3a}fH#xDf=>NOlytTf4T+-QT zN_IDFP}FhkwPvGe^IM+o4O_Y~l6vN(P3vY6t*-WI4T1)X!6mvuiXNN{SKo~vq0wsdbDC|g>=vj` zx(_Gh4L+m1UBbzZB~P+7|7|aUUPFRODxt*-X02Xax7o@J}X>Pd3F^66o8 ziXI61^P2bb^N$IfY;MO5oS75xeCT8aNj@6-4sGYO1~DUNO(smPrzX4~?n%juBuWWG zDDZdFUjzHwbooovJ+;@>LJnm`&%XbmV5@$$(%)}vc7nl7sqFtPpXOl0BT`~CDcyI} zq()-drSlLfE{AC@Rt zr?8?s*0wF^&a^no*?MC9SL6GO-mmBDq!}rXM+h7KX8L@_fzf6_#LJ0)mYs|kub}}= z={%wl~($q#{EH*WP1j~KOeC%;MdR~8GhZxX)H*G73T_VwhQU*=( z!m#CR3D3d)K4p%nIPi>5871g=O&tm1u`LSCG)~AADnGK2jQ=rPrs+0wN?WU(39T2h z-_0e$(2O#0uMF_zG0bo6zJVpx>d^hadlFrnv^U3|gp0)%S8R;+=+#K5U7#LuckFBy z@+(qBqm$9(xah)!Y=a{DO6_UcbhJ4DELT3`1;gJy!o}oV5Qwi4fjRMN%z)T{<#AsD z6?F1te_N-&EacaNl@1m91@Gn&ebBl1)d83j$GX$rx+z}$ypw=7E{dPaL;>EaTrRlfQ|2^n3kLt@xE$bF* z<-jX55=bI&tLDh+DzpB2b805E$G-)L$8Q(!+;iD4PG)WpkJm_@rCqF}JuaGRh8a`s zbSPq_VDRMS``krVtMdso$O^8!KK;XPs6F#Fs3n zHd6~mSZ8(!(vToX14+yfP}`g^~;MxE5@jTncIN9A~xA( z$fB$G|177DxivTfs}Kz4l5j+9S+=I#L=F7UAD*}6tR|B8I04hZ6H0Dl1lyZ_JyW@mrqFm7tam0+gE>Y z?M_(fmRGx}9o)CuU6B&B$o$J=&8$s@oLVsF`bd8-G5CBNwRGN+#P zPXDCen{)qJ1lcJPxy#};ur9W_2fe+`^Q9cr<%sL5@x0MI%`@b(E*($Wi`k+q=OryI zdipfX&6z5ev~m|wYfl7n&xj0owodyi?5h$AlC`! z)QNH2*)OHm0N94J3nibytRQ3dya0Nmv{a7^pSx8p-`=iaU5#y@HlpS8av_n-92MshD)%`>qNiL(EPsTh% z6|%~4q~h(hop;uwi&`STP1{i&`STigvOb^^b&mLYC+M=7IaWg>V_da)pmC?t+{!Oh zwJ0>CL?vy4twDN4n^_b-F_I}fcsm)uDWo;dC9XZ`F4x55Uo4XtqQh7p$6oS0ZGKfFM5mY<~UmQWoyzpxc3pOMIxAcTuQ;N|7rRP9bdp*3Hc zZ~Tqa2>86zr9YjaR5Oc}PM9}tF=;u3>Y}-o>Fu4?VBY8F+`j!InU%{LFL^D~3}MFz z_LRn+&OINx*;6^P8P}W>>h>s(pp4IFGd?ZYr#NeB>ctRcqdCBfiq@k&%MZyW%Z=Bq zfNJP%Zq+8dRCTy7enIAeljLTMd^Soz1E+xs`dkrrn^8wk=I{0EPlhrGTtb~%6uHZ< zAvGtm^)sPrv-KEfur5({tuFf?jgCJK;U+JwZnK$OpwVT&p&c^3fPvDHef{e$r;ly*Ta`)q_%Wy;NzfxMh?C*C@dcPP0BgW+ z6w9&p9F@NeZ{zi$elz@t?9RXT3OC&D_*1l0(nw}TjIb_ zx}E9w2pkQ2;de}~WaS!jRi&d2rQ1U58Xfn<>LszQ<^b;X)(Z_LYj#grmP1uTw*3o7 zNg7kmz0)T)D_@mRTY4Ts+9=79GRs#HmbLoldKiNPj)+Nm{DsMsw`?Tz$6e%o`sY7; z%*+XEHY;;{dGlQ4eY@^r^TN)?l*7k?nWt^^Lf;BYvnzm%AT%#aO`jY|-ezr{(j-wf zqBDlhQb+I&uU2c01;_~VO@54C5Wk^|6>Q$>d}p&>=GJovk(PpHukm2=?sKWN)sO=x zmsP2%_I~4H#-zmT9X;{UYzrbJ`}mlSxTLcJ$C;uE^ClOtXY*s?Xr!;XGT42c*2gd$Ts-1EVCs_J7%LN_LuK=c7;`*K zGQ6g7NQCpoDIGqN!5wiT?3@a1Q?2t1T9-u6leGCGBs_g47ejMJk^r_ZS09cP%qxN_ z64#DsoR4zB@!)J`Ys?)<~ZVN`dCyB2vv^P2soOG;&kUydirewo?B*x z1%?JI0x_1ns-}}3A2S^oi2J8*^MATFNK~0nt;%L3*44?*&tRjQ+q@RipIUzCE}*ney{hvNc0 ze~tlJ^g(Z7wd^Q5_YeVj0+bpR9lQ>Rhw%W}(GW*^HYxZl@lBZB;y#|3oT-)0;dtC$ zwxxB^EUodpjh}IEy<8P1$FA$vsCznen8fOYQ!cD)50yHb?uilzlV?dX#Mqq>D$?=B z?6jiC1L(I?hUv-9NwHip6~D2CJGUBVS)8eR+%kh?t#uzy3?$|EP7WC0Pa7I+L5F|U zxTeu}v{NFlZ!%N`g^i&xozL%8M@lYly=&qzXaWb$tL^U-Luk)|)5DbK=TFuNc-Ou99EYc*VE01?BuUKTKfk?`CLxYgp^ooh%2z6$ z%_&BYsH`4niVAY@VX%B_SGu+wWKuhQ92Y9iTzQPGYHt>Dv@deWrW$dVOHPcAPT}Q` zj!x{TYm7{M;$G6YgEPR%7PKlFKw-GKKmpTflXDO#yBY8My)tDVwiDM4VI_=w-kQOU+6pnNF)zs!L;%weZg0K|yjv>>K*0L{qt_yYEw zK<-$Bc_{+$00b*wIxYgh79O-LOZMhQ@ln^|0JoMWyjW&mnzO|0Ip@xKXKPvxh790K zgB{_6!_HL%KMH<{2Ia;<8b2=Pj-*dYUog4Y-TVuXNy`fhysp6i?!EL*>XMDM^*5O1 z+2uZDI{-g6fUFg1k@PtZ0I7_ii{WMG0giLo2cdv@7XqA#@^&?3Jwu(mEs3A2%4t+D zeL7v=T-18~S*rdrb78Y(^PQ$=>8`3w#728ZNsOFsX6RE3$6Qk)rGCw!(X&i`Cq=i3 z-Ke)zn~DN0wYvGQB$rQ@Cvfpp$jYQgvFdUI}$D zu6P)AIUFJb#JS!t&%7jRa-i-0=hfT6aaBvsOe440MeQVVsNguoqm@mHq#t8$ZV8?9{w$F#qvdqOx7}_as&xhG+GwR4LM4P-C$vU z8jbm6}lh(LtiQ=W#=m22w?*inKV-X|atr$Buk`?iyX zg#yqL!8uRAw;teK8G4mq_RhNoK@RWSSHQW{(6p|BNu~XJl&|Hks$_R5M?>Azf{?tC z8!@rBU68WTV$|TgACZzzAgiP8e!IX0HK-JK8Ej%@K&LG>!p>LoMrm}S2<~`n3S1NB zd1cu_d1vEdr%4l{6&dAT>BBjkO8PN5aRHVR$44On6!BGsLOCu!nZ-s#L%A~;SeXJ6Mi#m0a(&y}QLtf!N8jDF*--=wvwPb9fg5Zw%paAtd>a+v5NPX*uWxha`kf3ba>-K={oE zKc+4!?^b&QGdv5RSxcdR09f?`$Q%EWF%h;oVx$M-PZ;oiK3JB{F&NJlAGHC;Xkcmf z1MjULP&0r+1ylBqz&QmY5<-Rzz)OK<@=AFcVDy((T&}|d))Tl3#1OzQHruqm1oTL{ z8Uqmo84xX91n&iGba>61DzDfc1%@~R{w0z1&jKZYjo!Clv=r(nEhW`lUvJmuX?(}&p5mUYblU*9>wptE_ajQS zF2M9q^SOU5_!$O;#lTkyD=e*Fa8ZOgE1I?>&>(*_1bV`$vO=c$42~v--4o=e^_}%A z#GX92zH12iQ~qsM7I)_Y21mNgj6{YT9GL@2+9C4ghUf{I;|3zX5VBzO%UQ=kV%d;KpQUSH_KPcYTn5HsDGi+v=UcbA<%-^%^Yw|k(mm%UhY6M z5~r|lR`?i?R|14HRQ{z0@>(E(UdNd*8ef{duUe53gzg2Fi9t=camCcK1+w>Gl;SMx z5T#!;pX=eb(ozPzWzfHCXl%rwMppnXdY%4QTB2}C$Q-TG^8m2ZLJY-$B>=Wd1OgA3 zb^NU<1bkdMbePeGA5%bl?wyprqj>MgkEZ3`}8~ zOUakg3Zfu@A#OzY(r;Q%gPQqXWfr2J0#qzStO7{Pqd=yFhj0coSP-=sfNwtQ4%|Z2 zE8u_qfi3k9F$Dz*7NC*#dcgtkHsaw58Zxj8kXHx13Brj1_ep@WIbdbO-mCx>>Kx(& zCjaC_);TxX7acwTlh(X35TYvCHUOe3gbeKSrHP4&tnqE3<*zEc5S6ZRMuHx|-|U-W z9FP)_g$au2pyH0f3qD-U5wN%kPYw# zlqL}$r*U_#M?FB~0t@kBAbxV9z_JSg5rE6iQ4|>E{stKAt*~d_pxS?~-D?(35gG*^ z@ngUk3mGJ5MI>ktZ$t!^v;hlvVZM3_fs?}{L+Ar=TrW9Z0%|LOgy+G&laF?bY(R!) zbOn@c{s3ug0&Gyhq}{0>IY!7~Mf`$6Cqiblx%*`jB3A7CjlDrGSrj8^8Bb`JKTd0|vo_ajXvoNi{QveCY z8(t|pA3r-}a8Xuw)HCN{B`iE7=s!bwNr?|#tsMd%f)EC%`@nN$KVK{h2>SnliviYE zQ(Y~@k@^*wrH~GQ2nynujKCj5_bs%M1qN(>-jpZeJqM_!J3#dYL$Xr<9Q_J}8la0` zDC`6aZg?WX8G68Xeg!f0PD2B=!>Whb13)%`ZKD|D4M6+5LG=oxVJ%?W0r3pBnot&? zSN6eo9XLG|I}>?eS>J!{-(SFzxB;zF*eJgxXvbi$>6Sqi9eiJi>Y;KP?j@K9{{&F3 z8hC#LlEeq-dC2qD7cRk|;24|lNcGgf)eH{v?SP~KO(oO-8#R-W`M;GAhtt~g3du=H zX!>98Pai#HZjd!z`P(cInQY8}Dw=pz0%SL_L$7qHn1zrF5n35D67-Egi-*O6*MbY! zA1KOm_Z?pm^j+i4#d_H06%Z*Sgi80h8YL)EAC2Bw>iC4%{c;B=;G>J9aJ3 znkRUUMg!5xQ2JAu*7dLZi-z^PLo{jUTuN9Z>Zs#WuIqd4Jqj!a8WpJQz_lUAc!Mis zMiZ!wsoDfYwg7hZXSl5<3y&YbZUYNreFYA`XOA6uECDwTVoX4i{06uwgDGbTmQ~=3 zz3&WBo;5=Mlq`NdWuOR81EK*es?6z^RWk$^Ik(=)J-_OFM`2t=D z=NQ5b(4?ZkTM+bS5EhDh(6-)kC_+7jOUxhKe{^aKF8d%<=IB&2phw&o{X-8dp5 z>jSX5Uf^&czIK4!RtLT`Sd{+)s`Acht*keeLRoN8p9upX9?-hUf7=BH20A)A8XKo> zmjV7+*l}H{PB)v|#@@aU_MP#3rS1J{Z;+sA1Yrot{Z%BA{r|pDU}~dc1J^0|{7OvJ zH#LD-Qb1gNU0udwHPNtN5Q<@|RT~uqftbr*2sCid!;RjBh-akgo`MaJ6u@wqgYPqh z3`C^}Jn60i6W+K)^$UPQ!Q$@<(3lX=>9CAkL5hGs2^Sg~Dkp4MeG+b~L4_$;%UdCH z`T)@dc0?2iM}YQGiJkyhR&D^(?#f)HK5~!&tY-`CD0};&ad!|~3Z2nB3aIU?z$grd z`vR_aIM;dxdxCimLBK~nQ>^>}7!ZJ^vv_g;YSrZv3bztQRsO*?hVu%W3_(R%w7~5G zpgwqNcAdVqK(1T9a}JcCfWkmOB{C!RjrW_Utra8}WC&aC?|#D_3VI#~klJV$&O=Vv z|L+SrsK9nKd5`!^f*cRzP0a_Hy^O^C>dSyIq-B#C==9S@1pL*2mSiOK>B+Uf!Egit zX$k>h7+92VfJp#U0}ycXgm?cBZbr`Jhkt?dekNbW1=NRN*B-uj@DFy0ot0H5Xf`0> zuDRL7&LD6aT>21(0}zWv??6pt2dfJ-N2q#WQ9yx6_E8|>h&)WECQ)Dnt)3GAsz*!| zL2m@%UqVs_%LypxfQyxbZyh z15lKKX?>=~q{RKtpz&Sj{~g%>_wOk(N_$>Sejal`36f97&e8s`uBTjG>u%P#zkvZ_6@`)gWm7Sf~)l%4d zAp!K??7h9~jE2zB+Jr$iV9jfefY0yBnDuoD}snusGvtUOpg z!y~{~33t+rnAm{y&Uc`j=Kb@_QTm-`B>H&O8-(w`JsR1Ps*u{WGa#M#SR&ICx5DZY zus}xz04r(i*y3f#Q$<8-k6w)D{?Q3|=4bAIx*Ul_5$-~c&(i?9WpgusnP(&<=r^JQ z+xhDERq=+8pa(;7F7zo8OUd!+n=}dgl^zBBjvz4T?BQPT*>3uH%K9TiorOWB!{9oV zA~CDklD6^X@ZsQPzo3=O2OcRZX~Qb#Q}pE;;3vvmY&dg~Q@%@)A zxGZ6e5iKbgp=f*4YQB_Gh9cbr6hrM9QenwgccEFwdRPDofdQa#(AvRsY@siS#?{~GuBPQ=kt(^&pL z#K#*E8(=#K$$u!lyiE)Yl>M%611kY!;_B#lZ)wEOK8)+Mx~q4h*B8O z%Gx!i!zW6hh=$ZQ_h&b+;HWuf&HSBZD8r)8^ABfacy3BVCDyQOAPStYnKNTGk3emPyTy`Sr|`|Dm43(y?~%l|DdK8Bg0zBL9-G zDePPzEu`MBU`&19j*`z|Ip?BjTq)Z3{u`ZFA3Da`n1~~;h`b053ev1+AMfHB%zjd= zgC5yk=XfsK1>}_+N6sDibdKeqrY{@PGVN2>N|pB}?sHw+T(z8DTj0|E&xuTJy?jUL z8gq56VvkmVvmnXqedO~HZY}b7p|``JcdI7=Vqv)Chj#k~1`HoSX=%ayj7*8i10dK? zvq~W^q8-X`%@N2#*RV^A_LB~V{%;_VD7}bm`-!fXE`=8))CWI*%-I1X<-03+k58DQ z0CY3ZoUzHw^UiL+ymm-hMa32hHaPlrPC{)G`8h^HJ7E(Iw*{r2!dB*bWcGhvj z)bf8nX*Vswm#%uWng!M z50DH?r+PZn>Hn_br6f*E<}~Y-9t{vLoB13mmo*Ln2e@A2fyLFVxa=dvvFP9RZ$MJ1 z4kF*bPDUc+4Pn2@HTbl}a*`fxNh)W9^&lz{^vJ(p{@WI?j%oY-3#_Kdf-b7mb6RSW zDay;X)YtoD?MwYn~xnWW)@Lv94CCc`jcyC z?he95wiuEUh^XqYu(D2cJgt7fSoDpVh(hVcXbiIo^5SqBW&>1IQ`ZO~j?0~gzb}gO zftA?X4F>CzOcGB0;Q+ixWFO%c1;<_Y(yLJBg7PPlW!Cl`yD8E%KxsK?Jx>{+6@p%^yHm{x%d(4XNT9r~~KbxBTOh%u zeN4^DMOX)>A=_78-^mFR#1S~2#rlrTd|~fBskQ8}(o2)I$|<+NB2Zp7FdEzr0lHMj z@>2;E|4ykD=TORhlZzEBu8b$ppCcv5z7_>aj?XKgHWAErI>9KQk~ z=ke~B;}@3Rz&l5br=ijU<_@f-BQX&__9le&!w|J}KeP_m+z_ua-J3jrKnZOkrHv(k zVTTM+<0ON z9wl<|f>iPg=vtGb#@+7#OHIFM{O81m_ajIqC zW{N&LX)$C=-L|Mh8<=TmGh`6JT&9W&JC|JhnFAYxvwJ4Ada35>H7@pL0n7-Ra`$R@ zxMJJ<+sQZTm!{QsFVS%+r)OttV{Jim$}GZ_A=VQ6FI9|`sqK~YJH7E~xN(u^`M=qE zA`D3c8Xa_dWS7mO3lF)l*6 z?cwf@7q>#JXfHf#--Fu8)zvi$>LMtTqZ1Y;PAp7t9y_|*59uV(fdja#>&&Y_evW~- z4uu7jhxdx36))b ze*RD>)2HJB5))BSV;zfpuW4XF1x>y;Phl1S%*c<1S88sRKsPBm8s+s6DzY<>R>Ge` zaUBBub)AaV_bs$s%BBsAZ=bg9Laq;*KoDRoKsR}qn6eQQsi&>QJ$Ymb0FiG8~l0|$pH6zJu%E7pVN{cv7k`0*FE z%8AAMNgz`A`IZd{za~bK9{9y?p#4yUZ~`Gpsw=qd-1BKBy`YLhksrFF!gNa@9(vz2~bDxo5j3p!5ssgN&-z?sSQc+4kl8 zaNol_3{mEiZo63TdJz!4lqZICk z7Ba+nc!EA4GzJeiPB;u|wD{K4GyCq-x707l6#$e#-E%S@-miN2dyP5^Mk1A?bW(1) zo*k9)BJ8??saMcl2;!X!)hVHrruTWus@ zbLgy^Ykb^*T>VLj+A>63_yyn2@^Vk=+|a}6*qU@=@j*JV<3RxAfp zsYHPN2qlQToLqT%d8t9hLsv=~8q5!)YuB#jP2^-{eUh{GpZ+EU(ljL|N2~l4IHq8$ zz@~W+dSFEy1Bt+^k&#rh#sX&&0|ZF!?(QQGA8K}x!FLx{A3G6_V`tY-(q<;6>SmRc z+;bj`g(9DkI5#&ZX5H+t!bx2bRx_^xVi?dN)~>T4)}7VQl#`clu&6V~ycIf8tfHa< z(z4mv#f3KDod;k7@*is#KO%Pt1&(=&sMG4|6S0E*C zX=y1gE*@G;F0T^F%ta@_Nce?y!(zm}TOPLhnNVXjpL-zn&m>uc-6O}~3zWM)3Wd=7 z;870HsF=majIvxdB$K`c5_~MlJJ6s$2fcLoKqzi+6O%OaI*H6P60vlR*ZeDW7}|=W zoISjfy5nEmv%~XcYO@vEIJmo>xLm>U;}wCuGw#G)Sr(}s> z0WGVC@ZO!kqYP+o6|-Kv)RelL0le|V45bE!r~w&|-LSqLxyc|;!fyw@Nye|$=5^X; z*}->ha^vxrs?0^P=tqGcg=mIFat!n{ou6zp$F7OtsS2y#t@SCvrbU$UWMyT;`ndZj zwk_SDD-28NYReK;Lo*z}wG6E_==|mzRYMO5G?2k&bg^N%@6tP1z~M7F_**@Fu;8EH zuBlmH_AacZ=T;R+dI?+Aiae;l`!_nC)h9-;Q|1zdGh22v(YDz8!3dp01dr{#m7**7pr$D7;mDTx)9at zTn{EjMxTXRSzu!~S=lEJVW?i~h$g89z9i{Z$QBr737_#_Zsz6_P*e;9rwO18igNgA z8)_z55sYT#nPPcG@JYZg1TDd%N9lRoZF6@oU(V!_)G3qsY}L>KJyax9!15Ubh#lz4 z(1W<~!w9R|a*Kx~sIQH?!HX*t!s`g!z%sgcWgha(w9srqHP@sT41aOZB5I|u#wc@L2FTqk+eVl`qZ3480;|4dLwi;w&-VHY`P7L)s>aC7WJj2ylGnk`giS`R7*_o4dCSa>NuAvUe|=;koIKPTZx6e+hTS;Nali-X0X^utP6|hby0q91|ad zc01j(^pCZUnF6)=w}*$0)kf^!^}(USzRDcB$T~VY&_j8zEsm#xG6wW<^V+;+z9R=H zo|^UJeH#i4rXNH&;AnBHN3_rH^+RF_kuRB9Trprn15`fR+ZD|4I%boTlNJq&x)rT6 zYuZ49{FLR{)&r3Us-glA#-87UKZK(jzaw-qH00pjyW6?B6fO@63JS_a5=bO`0c#V8 zg48eW!v-8krN&}K-lWF+yavgBXxFFQym{9YX%n z;MmtGl9Z8=p;Ov=C$Wl`grT5-Z~+;R9nv}i z@w3Xx%4l(eItxVH9`Y%l4vycp8}}mMrh^^(6tu5FijodEw!*@@AR+{B^o)s#l?a|A z^%bmKvHF!T4UGYq?8>^1B*Mh!3!H73%7Kigxdw6)PGu(lj36vxu!a{J|fIANcRgD&6$Uwt+5Zz&yy6(Si zsdZ=hSH#BF*4DuRb>^_7UAuC`XOZuv(17LB&+O{D1n*(W^Ao&TW~ztqlcsNlRX626WQFb%lBah6!kBL~g#dq3};ODORk?;H;FobfChwjE_Z6%blcf07RatqWbYdoE^ASuFkI6U>KsgAH01_{N%d9`R_78Ni zLf$^Ntuk-2X(gzuuk8fI$Hyb>Wn|3%YN`n2m-Ygbc`KrTw~cg`VN?tE{Ug{u&?AN| z1=}R*VG=V;x~#==pk8Kq6o`OsWCj>FrgF2F++g+G9|Retm46R5l)kXgdBCt7_NX!rjL%xbi7nY3fEVF*47#bGE5~2F{Q?!SJ z1Lf3n=pMj$YSp(B5^3t!?zz(EPQo%m7;x$flT=ap0k%+(&dH ztr1jSKxPm|2MAs&$6yCkY&2IJ(ml%u9mBuBchS?+3keAUB|0Ev^Hsw22m=WCXkS1C z+tHoFo+L^B{1N;J7+sV?6#!5?RhYTMCxfX$7lhUFOHBWjjDg`90R%{R;Fs-OZoXrs zs|>qcmLAFI=^1|yhFm`XnD^%X!=+XY)=hScaL&O=;T|}!Ky87@Exg~*gwha}Iz)91 zCRgAKLdi1w=+Q2872^^;e$+rNk4Q3XY;16=g~67ImOSNO$by zZ&*9HA8;g}#rjm3RKi#V?vt{zGQ18P2yzSt|Fzx{OZ;1QS6k-9agJ0tzA&xjd-4hC zWq=q4ucAAkXJBr%aXAX+x3Gn-{h-~?@-d8YzqNY!O4%QBFW6*E7|qN|_zbWut?Mnu z;qyV!3Kv_n*H?I^a0~$W6mordGpNUXPs0pPy=ELamt191qZa<&8Kwzdh3kEnE<$N^ z?qkuHl*4-Ss>dnp@_4HF;m!1m4rV?QPd*`L(7OT3FgKf$NfFSeh}(ooju@IYn_TtM z;WN-CkWSlT0llQ~53c%|RC~se=ED5?b4bGo06dB1P&Eug(x*{x2)kl)AGsne4au$= z!qM#VTNO%jh2slik4J3z_Y3Ij>z@Uz$d0 zh03+VGhXM+rAb}WcJ5hw3qzEV(NRV3xov3vAagP}nh+L(L5mvHqn;EVZ-AN*jL%-c zzJl}bRrnV2Ajm`EeoDV}i*9g5D9d{=mK;LOmU1(M6$G9*?rxx`_Z>vEVW8SzA@(SR zeJiXFMAx7o%7?ob+$ZK-TQL|6*!=9m$Nb+dd=_p@Bw*#{zS`M=)w}~J)NBYfkiJ9W zTrFbIu*jnt@)~dn;6E}3T|aUj+HaWvu5`9p| zZ2s}D?Y3dqD<9_!JhzCP(ub!P)?D3MW2ChXlAJg{`OS*2Kn*jH;OsVQp1zwpEY?k} zgG(8B{I9lHBU|F>ZM}qx!m-*E%e<0?nQBknAK%p0WHRB6)xg}qzPxRcQ7&?87p`T< zsy(NR*}1rW1LO#ll3>Dw#rP(`}o~>)x~L;6Co2>k`nH`9mIm za%V?o*_v%aCPTRI*UqbO#eQsJe-KZ2XRFuq5eKD)F`Dl|Ys&d)VwtHq0+jo$_;HF? zhs0zjPCkpHx6FT3HHiGzcuA;wA8C+0nJftGoSaImQYV!Bwbv=W8Ty-K^xwd0GJLys z*It-Zo~V&%FAyR5G(4@pZTH)q|BtLU53Bj@-oH_4H%LfA(xzD|gizF0sc1w4p-_?} zAw@(3$&{!>qDTluLJE-~!bU>IP(qo?JpNv9pZoj#^K^Xf<2JQ>57)KUxz6)k>$>#I z`rWp~SaI36(yvZB@j0T;vx?*o_bHxa#eS!#AemVj3tu55jBX~r7SZ3r0s~*N3fURk zQ&K$6L`S7p>3WUIpuAbpR_0pZf-5Py9oow{SK~^6mg4ZA*|TS}nubSzGplqwFyX+3 z3m2FhqCI%B->`D^! zY_g7L{-V;yS%X!nf@?zv67K;I;*>7EZDc=cw%XL(@I6wiTv0#8d!N70-J)UP6-qys zc6(&lT{=WPW4rgvP|vnQRW6t&&75JF)Naa?KDFi1r8kM|;$2w*wZ1Da*}Y%ACM_q? zD%WmT>nGdgDr!P9 z>kql_Zm;1|`k1u%_R}W{*itAfOe!Zx8^Oh=9=~HEZZoi#rEOewBXd}h?V8T9+Qtl# zW8XSDIMhX*Avtkf$7#R~G9SJ}<;TbkFJj~3*g%v|bBbz-F)tSm|Jcb^Ro3SJ{u7&~ zwpSP1Zb?khn>O{%hNpS@%Gt9e?zUmHq3sse<>!bx*-2zu{1;R;b!`sabn{N{1Tj*$ zBzuJ@IIA&xLj0sd$7@6MHXBmj1=N1}v@lGg6tf>|WimJ(D*JozgxP9yx0}##DZby^>XeLy0drh-XBIYN%O?R zW$&L@<$82iS8u;5BlN^%ssa1HtFw*NlGd$L-nN&`Y40bN9bBjyujcFy(^aWg*VSt* z_h~pV@cDbCDUE}>1Q~MI9gJI!+WFm5o4;nytnRDFYR|bb``tqgZdN~Db?UDfJIr?6 zEJ)cc{FRRK=RA?IyIOYdZg!gje7KUV#wjN0PPvLoN^x)c6D`#2Y+80T&MA8wI!i-u z;6RTQo%AX>r5!GLE_s7Alr=AR5~7vZR?RN2`pSIYnKN~o6{BTiwJEpL0`RH2h*o|u zkt8xmRbwxZGHdCo5ZjVE?dXQ4rhbkd?wvnz^YA z$NktZiq)>yb<^Cu%p>bT@!zbB56$0q9+aoc&If|}RW&De3k4<{JAb*PW3222+ zORS6`9Yaq%ynI>z-bh`66le+NS{8N+04MD~80x@Et(ann|HzxE;3J-VHvn$IlxHP|lEHc{I{`S%vXMBjO@r558i zeVr`Tbt@0RaM%LfJCO!=)&k&uL7-1nJ0r~ZIb_5`1_^^DY=Eb zZYgDnpFDXI-Xp&zxYom3$J91ZA$FIW0oyVSjxnTm;MT@89mf=%RsOqru3kRXbNA}u zufscbEWLi?2GOMzx%y^OM5KkwQ6j5pIkMPmcu6$*W z^?JIx0aSzl{@AZcs5`>R0i2*9yIRe52GWP2nVGO0fo;&ZmeLs1S87aQ;AaeuGb~t> zRO3-xU;vzV-}=;CyMP!+Ey|rOKcK7_t1W7ox9)p~_7#bp`eLk88oNrR*8@rx-gR&> zWf26%ad?IG_JddeDY|1jOX~glZFm8y{DOzA)l$Hb%u?$lA>8-u-o190jf_YbmN}Ps zws1YWHm^22EV!5Ph3dzLFgTMsZTa%$!Xbs0kzrJqBe6}B(!T%rL9KK|lAs6>hJ_KV zcau+;sIQQy#tS=No3)kGb9F(fnp9w~5+=1O!ZR)~IbEaSKpL%+DvuuhR zKcMQUzH3%9gK6#US3P|D1mhb-fJn@Rck0v$R4wyy-?x=q(yw$bD-QY;i!xPg z*K3)#C>D?_gdSCog|qH8_$s@r3G=5z9}kyQ!XU4+fAJ6OpIQ1flzJcScgjKU0Y0

    9I9FF@P7w|33pogDv+B75Br<0+Dt)Ws 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/W0 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%0AP=o$huQq;GE4aX2M=W(jL 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 GUHl(&O|04g diff --git a/tests/gui/test_window.py b/tests/gui/test_window.py index 491200b..da00241 100644 --- a/tests/gui/test_window.py +++ b/tests/gui/test_window.py @@ -158,8 +158,10 @@ def test_window_home_lists_recent_sessions_that_still_exist(qtbot, tmp_path): assert opened == [str(saved)] home.open_button.click() assert opened[-1] == "" + shown = list(home.recent_items) home.show_recent([]) # back to the hint assert home.recent_items == [] + assert all(old.isHidden() for old in shown) # not left drawn until deleted # -- the route ---------------------------------------------------------------------- diff --git a/tools/make_docs_images.py b/tools/make_docs_images.py index 9892d65..9b72e19 100644 --- a/tools/make_docs_images.py +++ b/tools/make_docs_images.py @@ -1,17 +1,17 @@ """Regenerate the images used by the README and the documentation. -Two images, both derived from a synthetic retrieval so they are reproducible and -carry no measured data: +All derive from a synthetic retrieval, so they are reproducible and carry no +measured data: ``docs/_static/hero.png`` The 12-panel :func:`croak.plot_retrieval` summary — the landing-page figure. -``docs/_static/gui_retrieve.png`` - A screenshot of the wizard's Retrieve stage just after a run, showing the - plot and the numeric read-out strip. +``docs/_static/gui_home.png``, ``gui_retrieve.png``, ``gui_analyse.png`` + Screenshots of the GUI window: Home, the Retrieve stage just after a run + (the live overview and the numeric read-out), and Analyse's Pulse page. Run it after any change that alters the plot layout, the read-out columns or the -wizard's controls:: +window's controls:: uv run python tools/make_docs_images.py @@ -42,10 +42,9 @@ STATIC = Path(__file__).resolve().parent.parent / "docs" / "_static" -#: Size of the grabbed wizard window (px). Wide enough that the left-hand control -#: column and the 12-panel canvas both stay legible when the image is scaled down -#: to README width. -GUI_SIZE = (1600, 1000) +#: Size of the grabbed window (px): a common laptop display, so the screenshots +#: show what most users see and stay legible scaled down to README width. +GUI_SIZE = (1440, 900) #: Pulse energy (J) handed to the GUI run. Without it the read-out's "Peak power" #: column is all dashes, so the screenshot would show a half-empty table. @@ -164,41 +163,55 @@ def _wait_for(signal, timeout_ms: int = 120_000) -> None: timer.stop() -def make_gui_screenshot(path: Path) -> Path: - """Drive the wizard through a synthetic retrieval and grab the Retrieve stage. +def _grab(window, path: Path) -> Path: + """Save ``window`` as a PNG at ``path``.""" + from PySide6.QtWidgets import QApplication + + QApplication.processEvents() + pixmap = window.grab() + if pixmap.isNull(): + raise RuntimeError("QWidget.grab() returned a null pixmap") + if not pixmap.save(str(path), "PNG"): + raise RuntimeError(f"failed to write {path}") + print(f"wrote {path} ({pixmap.width()}×{pixmap.height()})") + return path + + +def make_gui_screenshots(folder: Path) -> list[Path]: + """Drive the window through a synthetic retrieval and grab three views. - Follows the same route a user takes from the Welcome page: generate a - synthetic trace, accept the seeded preprocessing, then retrieve. A synthetic - session is used deliberately — it carries the known ground truth, so the - read-out's ``truth`` row is populated and the plot gains its truth overlay. + Follows the route a user takes: Home, a synthetic trace (Continue generates + it and checks its marginal, Continue again prepares it), then Retrieve. A + synthetic session is used deliberately — it carries the known ground truth, + so the read-out's ``truth`` row is populated and the plots gain the truth + overlay. """ from PySide6.QtWidgets import QApplication - from croak.gui.wizard import Wizard + from croak.gui.window import MainWindow + from croak.gui.workflow import Stage app = QApplication.instance() or QApplication(sys.argv) - wizard = Wizard() - wizard.resize(*GUI_SIZE) - - # Welcome -> synthetic generator -> build the trace and advance to the - # marginal-check stage (see croak/gui/stage_synthetic.py:advance). - wizard.show_synthetic() - wizard.synthetic.advance() - - # Preprocess runs synchronously from the seeded defaults, so stepping onto it - # is enough to produce the cleaned TraceData the retrieve stage consumes. - # Widen the spectral window and lengthen the time range before preprocessing. + window = MainWindow() + window.resize(*GUI_SIZE) + window.show() + paths = [_grab(window, folder / "gui_home.png")] + + window.new_session("synthetic") + window.do_primary() # generate the trace -> the marginal check + # Widen the spectral window and lengthen the time range before preparing. # The seeded defaults regrid onto only 24 frequency points (dt = 10.5 fs), # which is too coarse to represent a 10 fs pulse: the retrieval still reaches # R < 1% but the recovered FWHM comes out ~18% long. These settings give 98 # points and bring it within ~3% of the known truth. - preproc = wizard.state.preproc + preproc = window.state.preproc preproc.lam_min_nm, preproc.lam_max_nm = 650.0, 1000.0 preproc.trange_fs = 600.0 + window.do_primary() # -> Prepare, which regrids with these settings + window.do_primary() # -> Retrieve - wizard.state.stage = 3 - retrieve_stage = wizard.stages[3] - params = retrieve_stage.state.retrieve + retrieve = window.pages[Stage.RETRIEVE] + params = window.state.retrieve # lbfgs-ad: the autodiff solver, and the most feature-complete one — the # right thing to show given how the docs now frame the gradient story. params.solver = "lbfgs-ad" @@ -208,25 +221,19 @@ def make_gui_screenshot(path: Path) -> Path: # recovered FWHM matches the known truth. params.reltol = 1e-12 params.abstol = 1e-14 - retrieve_stage.state.load.energy_j = GUI_ENERGY_J - # _run() reads the state, but the controls are only repainted from it here — + window.state.load.energy_j = GUI_ENERGY_J + # The run reads the state, but the controls are only repainted from it here — # without this the screenshot would advertise settings the run did not use. - retrieve_stage._seed_widgets() - - wizard.state.stage = 4 - retrieve_stage._run() - _wait_for(wizard.state.result_changed) + retrieve._seed_widgets() # pyright: ignore[reportAttributeAccessIssue] + window.do_primary() # Retrieve + _wait_for(window.state.result_changed) + paths.append(_grab(window, folder / "gui_retrieve.png")) - # Let the canvas repaint and the read-out cells update before grabbing. - app.processEvents() - - pixmap = wizard.grab() - if pixmap.isNull(): - raise RuntimeError("QWidget.grab() returned a null pixmap") - if not pixmap.save(str(path), "PNG"): - raise RuntimeError(f"failed to write {path}") - print(f"wrote {path} ({pixmap.width()}×{pixmap.height()})") - return path + window.go(Stage.ANALYSE) + window.pages[Stage.ANALYSE].show_page("pulse") # pyright: ignore[reportAttributeAccessIssue] + paths.append(_grab(window, folder / "gui_analyse.png")) + del app + return paths def main(argv: list[str] | None = None) -> int: @@ -247,7 +254,7 @@ def main(argv: list[str] | None = None) -> int: if args.which in ("all", "hero"): make_hero(STATIC / "hero.png") if args.which in ("all", "gui"): - make_gui_screenshot(STATIC / "gui_retrieve.png") + make_gui_screenshots(STATIC) return 0 From be7835b42598d057341c64d4131dc13a717b0ebd Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 23:01:43 +0000 Subject: [PATCH 15/41] State the live preview's real rate in its help Co-Authored-By: Claude Opus 5.5 --- changelog.d/gui-preview-help.fixed.md | 1 + croak/gui/stage_retrieve.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 changelog.d/gui-preview-help.fixed.md diff --git a/changelog.d/gui-preview-help.fixed.md b/changelog.d/gui-preview-help.fixed.md new file mode 100644 index 0000000..116d28a --- /dev/null +++ b/changelog.d/gui-preview-help.fixed.md @@ -0,0 +1 @@ +The help for Retrieve's "Live full-plot preview" gives its real rate: up to four redraws a second, fewer when drawing is slow (it said about once a second). diff --git a/croak/gui/stage_retrieve.py b/croak/gui/stage_retrieve.py index f779c42..e6e97d8 100644 --- a/croak/gui/stage_retrieve.py +++ b/croak/gui/stage_retrieve.py @@ -132,12 +132,12 @@ class StageRetrieve(RunControls, Stage): ), ( "Live full-plot preview", - "Redraw the visible result page (retrieved trace, pulse, " - "spectrum, marginals… — the other pages redraw when selected) " - "as the retrieval converges, throttled to about once a " - "second, instead of only the convergence curve. Useful for " - "long LM runs. Pressing Stop keeps the latest full plot on " - "screen. Turn off for the cheaper convergence-only curve.", + "Redraw the overview (traces, pulse and spectrum) as the " + "retrieval converges, up to four times a second and less " + "often when drawing is slow, instead of only the convergence " + "curve. Useful for long LM runs. Pressing Stop keeps the " + "latest full plot on screen. Turn off for the cheaper " + "convergence-only curve.", ), ], ), @@ -344,7 +344,7 @@ def __init__(self, state): self._worker = None self._base_result = None # unfiltered retrieval, for post-filter toggles self._elapsed = None # wall-clock time of the last retrieval (s) - # Whether a live full-plot preview has taken over the canvas this run; until + # Whether a live full-plot preview has taken over the plots this run; until # the first preview arrives we draw the cheap convergence curve. self._live_full = False # Last regularisation weights used with each solver family, so switching From c7777549c5d10c210f7846631cf2bc8e4db1ba39 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 23:01:52 +0000 Subject: [PATCH 16/41] Describe the redesigned GUI in its release note Co-Authored-By: Claude Opus 5.5 --- changelog.d/gui-workflow-window.breaking.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/gui-workflow-window.breaking.md b/changelog.d/gui-workflow-window.breaking.md index 69287e5..7bf558f 100644 --- a/changelog.d/gui-workflow-window.breaking.md +++ b/changelog.d/gui-workflow-window.breaking.md @@ -1 +1 @@ -The GUI is one workflow window instead of a six-step wizard: Home, then Data (load and check the trace), Prepare, Retrieve and Analyse (dispersion and uncertainty), with one primary action in the top bar and the settings or a replay script on request. `croak.gui.run_wizard` is now `croak.gui.run_gui` and `croak.gui.Wizard` is now `croak.gui.MainWindow`; the `croak-gui` command is unchanged. +The GUI is redesigned as one workflow window in place of the six-step wizard: Home (start from a measured trace, a simulated scan or a synthetic pulse; open a session; recent sessions), then Data (load the trace and check its marginal), Prepare, Retrieve and Analyse (Overview, Traces, Pulse, Diagnostics, Dispersion and Uncertainty pages), with one primary action in the top bar and the session's settings or a replay script on request. Plots are interactive pyqtgraph views with hover readouts, zoom and figure export through matplotlib; forms fold by section and say what the focused setting does. `croak.gui.run_wizard` is now `croak.gui.run_gui` and `croak.gui.Wizard` is now `croak.gui.MainWindow`; the `croak-gui` command is unchanged. From a253e8d4a972f6ae5d9c04b86dd5b9c8ac72711b Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 23:02:47 +0000 Subject: [PATCH 17/41] Offer Find a setting on Retrieve's form A query that names a section shows the whole section. Co-Authored-By: Claude Opus 5.5 --- croak/gui/sheet.py | 4 +++- croak/gui/stage_retrieve.py | 3 ++- tests/gui/test_sheet.py | 2 ++ tests/gui/test_stage_retrieve.py | 16 ++++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/croak/gui/sheet.py b/croak/gui/sheet.py index b4c3f0f..7f8958c 100644 --- a/croak/gui/sheet.py +++ b/croak/gui/sheet.py @@ -172,9 +172,11 @@ def _fold(self, open_: bool, remember: bool = True) -> None: def filter(self, query: str) -> int: """Show only the rows whose label or help contains ``query``; count them. - An empty ``query`` shows every row. + An empty ``query``, or one in the section's own title, shows every row. """ query = query.casefold() + if query in self.section_title.casefold(): + query = "" hits = 0 for row in range(self.form.rowCount()): field = row_field(self.form, row) diff --git a/croak/gui/stage_retrieve.py b/croak/gui/stage_retrieve.py index e6e97d8..2061c8d 100644 --- a/croak/gui/stage_retrieve.py +++ b/croak/gui/stage_retrieve.py @@ -339,7 +339,8 @@ class StageRetrieve(RunControls, Stage): ] def __init__(self, state): - super().__init__(state) + # The longest form: offer "Find a setting" over it. + super().__init__(state, find=True) p = state.retrieve self._worker = None self._base_result = None # unfiltered retrieval, for post-filter toggles diff --git a/tests/gui/test_sheet.py b/tests/gui/test_sheet.py index 46a5ab4..36c7cb9 100644 --- a/tests/gui/test_sheet.py +++ b/tests/gui/test_sheet.py @@ -126,6 +126,8 @@ def test_sheet_find_shows_only_matching_rows(qtbot): assert not grid.isVisible() form.find.setText("samples") assert points.isVisible() and not resample.isVisible() + form.find.setText("grid") # the section's own name shows all of it + assert points.isVisible() and resample.isVisible() form.find.setText("") assert grid.isVisible() and resample.isVisible() assert filtering.folded and not filtering.body.isVisible() diff --git a/tests/gui/test_stage_retrieve.py b/tests/gui/test_stage_retrieve.py index 041e405..3e84f26 100644 --- a/tests/gui/test_stage_retrieve.py +++ b/tests/gui/test_stage_retrieve.py @@ -461,3 +461,19 @@ def test_stage_retrieve_failure_reenables_run(stage, message, status): stage._on_failed(message) assert stage.run_btn.isEnabled() and not stage.stop_btn.isEnabled() assert stage.status_label.text() == status + + +def test_retrieve_form_finds_a_setting_by_name_or_help(qtbot): + stage = StageRetrieve(WizardState()) + qtbot.addWidget(stage) + stage.show() + assert stage.form.find is not None + stage.form.find.setText("regularisation") + shown = [s.section_title for s in stage.form.sections if not s.isHidden()] + assert "Regularisation" in shown and "Solver" not in shown + regularisation = next( + s for s in stage.form.sections if s.section_title == "Regularisation" + ) + assert not regularisation.body.isHidden() # unfolded to show the matches + stage.form.find.setText("") + assert regularisation.folded and regularisation.body.isHidden() From 4d8a6ceff6eeafd2ebe6224cf34bd1e5078bcc0a Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 23:03:39 +0000 Subject: [PATCH 18/41] Step through Analyse's rail with the arrow keys Co-Authored-By: Claude Opus 5.5 --- croak/gui/pages.py | 12 ++++++++++++ tests/gui/test_window_session.py | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/croak/gui/pages.py b/croak/gui/pages.py index 283ba0f..50ad934 100644 --- a/croak/gui/pages.py +++ b/croak/gui/pages.py @@ -14,6 +14,7 @@ from typing import cast from PySide6.QtCore import Qt +from PySide6.QtGui import QKeySequence, QShortcut from PySide6.QtWidgets import ( QHBoxLayout, QPushButton, @@ -276,8 +277,19 @@ def __init__( layout.addWidget(rail) layout.addWidget(self.stack, stretch=1) self.key = "" + # Up and Down step through the rail while focus is anywhere on the page. + for key, step in ((Qt.Key.Key_Up, -1), (Qt.Key.Key_Down, 1)): + shortcut = QShortcut(QKeySequence(key), self) + shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) + shortcut.activated.connect(lambda step=step: self.step(step)) self.show_page(pages[0][0]) + def step(self, offset: int) -> None: + """Show the page ``offset`` places along the rail (clamped at its ends).""" + keys = list(self.pages) + index = min(max(keys.index(self.key) + offset, 0), len(keys) - 1) + self.show_page(keys[index]) + def show_page(self, key: str) -> None: """Show the rail's page ``key`` and refresh it.""" page = self.pages[key] diff --git a/tests/gui/test_window_session.py b/tests/gui/test_window_session.py index ba3610d..e8ff49b 100644 --- a/tests/gui/test_window_session.py +++ b/tests/gui/test_window_session.py @@ -112,6 +112,26 @@ def test_window_analyse_rail_switches_pages(qtbot): pages.AnalysePage(WizardState(), []) +def test_window_analyse_rail_steps_with_the_arrow_keys(qtbot): + from PySide6.QtCore import Qt + from PySide6.QtGui import QKeySequence, QShortcut + + d = Driver(qtbot, "synthetic") + analyse = d.analyse + shortcuts = {s.key().toString(): s for s in analyse.findChildren(QShortcut)} + up = shortcuts[QKeySequence(Qt.Key.Key_Up).toString()] + down = shortcuts[QKeySequence(Qt.Key.Key_Down).toString()] + assert up.context() == Qt.ShortcutContext.WidgetWithChildrenShortcut + assert analyse.page_key() == "overview" + up.activated.emit() # at the top: stays + assert analyse.page_key() == "overview" + down.activated.emit() + assert analyse.page_key() == "traces" + analyse.show_page("uncertainty") + analyse.step(5) # past the end: the last page + assert analyse.page_key() == "uncertainty" + + def test_window_new_session_asks_before_discarding_a_loaded_trace( win, monkeypatch, experiment_h5, experiment ): From d32062c7742b79c215a84490debcabbb02f3ad69 Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 23:12:40 +0000 Subject: [PATCH 19/41] Bring the GUI's own texts in line with the window Retrieve's help points at Analyse for the full pages; the Home tile says which trace files load (HDF5 and NPZ); docstrings and comments name the window instead of the wizard; the mirror stack's range note takes its colour from the theme. The screenshots show the corrected tile. Co-Authored-By: Claude Opus 5.5 --- croak/gui/branding.py | 2 +- croak/gui/help.py | 2 +- croak/gui/home.py | 4 ++-- croak/gui/mirror_stack.py | 2 +- croak/gui/settings.py | 11 ++++++----- croak/gui/stage_preprocess.py | 2 +- croak/gui/stage_retrieve.py | 17 +++++++++-------- croak/gui/stage_simulated.py | 2 +- croak/gui/state.py | 10 +++++----- docs/_static/gui_home.png | Bin 48852 -> 47809 bytes docs/_static/gui_retrieve.png | Bin 196262 -> 194586 bytes 11 files changed, 27 insertions(+), 25 deletions(-) diff --git a/croak/gui/branding.py b/croak/gui/branding.py index e8481ee..1740dac 100644 --- a/croak/gui/branding.py +++ b/croak/gui/branding.py @@ -1,4 +1,4 @@ -"""Application icon and desktop identity for the wizard. +"""Application icon and desktop identity for the GUI. Keeps the window-system-facing identity — the name the desktop shows, the icon in the macOS Dock / Windows taskbar / Linux dock — in one place, out of diff --git a/croak/gui/help.py b/croak/gui/help.py index 43e6766..44ecf31 100644 --- a/croak/gui/help.py +++ b/croak/gui/help.py @@ -4,7 +4,7 @@ ``(section, [(label, text)])``. The same ``text`` is shown in two places from a single source: -* the scrollable summary dialog opened by the wizard header's ``?`` button +* the scrollable summary dialog opened by Help for this stage (F1, or the ⋯ menu) (:class:`HelpDialog`), and * the hover tooltip on the matching control, applied by :func:`apply_tooltips`. diff --git a/croak/gui/home.py b/croak/gui/home.py index 276288e..277a81c 100644 --- a/croak/gui/home.py +++ b/croak/gui/home.py @@ -31,8 +31,8 @@ ( "experimental", "Measured trace", - "Load a FROG or dispersion-scan measurement from HDF5, NPZ or text, " - "with its spectrum, backgrounds and calibration.", + "Load a measured FROG trace from HDF5 or NPZ, with its independent " + "spectrum, backgrounds and calibration.", ), ( "simulated", diff --git a/croak/gui/mirror_stack.py b/croak/gui/mirror_stack.py index ec93b04..3331b00 100644 --- a/croak/gui/mirror_stack.py +++ b/croak/gui/mirror_stack.py @@ -102,7 +102,7 @@ def __init__(self, parent: QWidget | None = None): outer.addWidget(line) self.range_label = QLabel("–") - self.range_label.setStyleSheet("color: gray;") + self.range_label.setObjectName("help") # secondary text, from the theme outer.addWidget(self.range_label) # custom-file sub-controls, shown only when 'Custom file…' is selected diff --git a/croak/gui/settings.py b/croak/gui/settings.py index 64f21c9..f57567d 100644 --- a/croak/gui/settings.py +++ b/croak/gui/settings.py @@ -1,8 +1,9 @@ -"""Persistent user preferences: window geometry, control-column width, folded strips. +"""Persistent user preferences: window geometry, folded sections, recent sessions. -The wizard remembers how its user last arranged it — the un-maximised window -size and position, the width of the control column, which auxiliary strips were -folded — so a small-screen user sets things up once. Values live in a +The window remembers how its user last arranged it — the un-maximised window +size and position, which sections and strips were folded, the spectral axis — +and the sessions last saved or opened, so a small-screen user sets things up +once. Values live in a :class:`~PySide6.QtCore.QSettings` store in INI format under the user scope, keyed by the organisation and application names from :mod:`.branding`; passing those explicitly means the store is the same whether or not @@ -58,7 +59,7 @@ def user_settings() -> QSettings: - """Open the wizard's settings store (INI, user scope, explicit names).""" + """Open the GUI's settings store (INI, user scope, explicit names).""" return QSettings( QSettings.Format.IniFormat, QSettings.Scope.UserScope, diff --git a/croak/gui/stage_preprocess.py b/croak/gui/stage_preprocess.py index 9a4d656..ccb3048 100644 --- a/croak/gui/stage_preprocess.py +++ b/croak/gui/stage_preprocess.py @@ -663,7 +663,7 @@ def _discard_worker(self, worker): """Drop our reference to a finished worker (kept alive while running).""" if worker in self._workers: self._workers.remove(worker) - # The worker is parented to this stage, so when the wizard rebuilds its + # The worker is parented to this stage, so when the window rebuilds its # stages (``deleteLater`` on each) between the thread finishing and this # queued slot running, Qt has already destroyed it with its parent. if shiboken6.isValid(worker): diff --git a/croak/gui/stage_retrieve.py b/croak/gui/stage_retrieve.py index 2061c8d..7d5b0b3 100644 --- a/croak/gui/stage_retrieve.py +++ b/croak/gui/stage_retrieve.py @@ -1,4 +1,4 @@ -"""Stage 3 — run the retrieval (threaded) and show the result on pages of panels. +"""Retrieve — run the retrieval (threaded) and show it live on the overview. Post-retrieval λ/τ filtering can be toggled on the result pane and re-plots the stored result without re-running the retrieval. @@ -62,11 +62,12 @@ class StageRetrieve(RunControls, Stage): HELP_TITLE = "Retrieve" HELP_INTRO = ( - "Run the phase-retrieval algorithm on the preprocessed trace. The result " - "is shown on three pages of panels — Traces, Pulse and Diagnostics — each " - "of which can be popped out into its own window; Save… writes the full " - "twelve-panel overview. Post-retrieval λ/τ filtering re-plots the stored " - "result without re-running the retrieval." + "Run the phase-retrieval algorithm on the preprocessed trace. The live " + "overview shows the traces, the pulse and its spectrum as it converges; " + "the full pages of panels (Traces, Pulse, Diagnostics) are under Analyse, " + "where each can be exported or popped out. Save… writes the twelve-panel " + "overview. Post-retrieval λ/τ filtering re-plots the stored result " + "without re-running the retrieval." ) HELP = [ ( @@ -1887,9 +1888,9 @@ def _on_failed(self, message): self.set_status("Stopped." if message == "stopped" else f"Failed: {message}") def _retarget(self): - """Ask for a new dataset folder and hand off to the wizard to replay it. + """Ask for a new dataset folder and hand off to the window to replay it. - The wizard rewrites the dataset-folder load paths (keeping calibration + The window rewrites the dataset-folder load paths (keeping calibration fixed) and reruns load → preprocess → retrieve with the current settings. """ folder = QFileDialog.getExistingDirectory(self, "Select new dataset folder") diff --git a/croak/gui/stage_simulated.py b/croak/gui/stage_simulated.py index 1155543..0675d5d 100644 --- a/croak/gui/stage_simulated.py +++ b/croak/gui/stage_simulated.py @@ -474,7 +474,7 @@ def _on_raw_direct(self, _checked: bool) -> None: self.changed.emit() def _set_can_load(self, can: bool) -> None: - """Record whether a trace previewed, and refresh the wizard's Next button.""" + """Record whether a trace previewed, and refresh the window's Continue.""" self._can_load = can self.changed.emit() diff --git a/croak/gui/state.py b/croak/gui/state.py index 9fbf1bc..c74c77d 100644 --- a/croak/gui/state.py +++ b/croak/gui/state.py @@ -3,7 +3,7 @@ ``WizardState`` (a ``QObject``) holds the four stages' parameters and the pipeline outputs (``load_data`` → ``tracedata`` → ``result`` → ``processed``), emits change signals, and implements cascading invalidation so editing an -upstream stage clears the downstream results — the wizard can then never show a +upstream stage clears the downstream results — the window can then never show a retrieved pulse that belongs to a superseded set of load/preprocess parameters. """ @@ -45,14 +45,14 @@ class WizardState(QObject): - """Holds wizard parameters and pipeline outputs, with change signals.""" + """Holds the session's parameters and pipeline outputs, with change signals.""" load_data_changed = Signal() tracedata_changed = Signal() result_changed = Signal() uncertainty_changed = Signal() status = Signal(str) - # Request the wizard to point the current session at a new dataset folder + # Request the window to point the current session at a new dataset folder # (its argument) and replay load → preprocess → retrieve. Emitted by the # Retrieve stage's "Retarget…" button; handled in croak.gui.window.MainWindow. retarget_requested = Signal(str) @@ -123,7 +123,7 @@ def has_data(self) -> bool: """Whether the session holds work a new session would discard. True once a trace is loaded (or generated) or a retrieval exists; the - wizard asks before leaving such a session for the welcome menu. + window asks before a new session replaces it. """ return ( self.load_data is not None @@ -209,7 +209,7 @@ def from_options(self, options: dict) -> None: The stage widgets bind to these dataclass *instances* by reference, so we copy the parsed field values into the existing objects rather than - replacing them (the wizard then rebuilds the controls to reflect them). + replacing them (the window then rebuilds the pages to reflect them). """ parsed = SessionOptions.from_dict(options) self.entry = parsed.entry diff --git a/docs/_static/gui_home.png b/docs/_static/gui_home.png index 85cdb8db10dbdc7c7fa61d29a6838e5a3ac824d7..45c7539671a9d3b7cd05c421c4f6ce33abe79adc 100644 GIT binary patch literal 47809 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 YdkI!bIM?@np$XIx$Db;GIDPTI0Th+Tw*UYD literal 48852 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

      E1}Qh1uJJfc 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 From 4578b1cc948b6d13119fa736fbbe9f0ae9c9037b Mon Sep 17 00:00:00 2001 From: John Travers Date: Thu, 5 Nov 2026 23:12:40 +0000 Subject: [PATCH 20/41] Document the workflow window The GUI guide, the getting-started tour and the developer guide describe Home, the four stages, Analyse's pages, sessions and the plots; a new living design page records the principles, palette, type and layout rules. Pages that named the wizard's stage numbers name the stages instead. Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +- docs/developer_guide/architecture.md | 22 +- docs/developer_guide/gui_design.md | 145 +++++ docs/developer_guide/gui_sessions.md | 167 ++++- docs/developer_guide/index.md | 1 + docs/examples/thickness_uncertainty.md | 2 +- docs/explanation/uncertainty_estimation.md | 4 +- docs/getting_started/gui.md | 61 +- docs/howto/collection_aperture.md | 3 +- docs/howto/fitting_thickness_tau0.md | 5 +- docs/howto/geometric_smearing.md | 2 +- docs/howto/gui.md | 698 +++++++++++++-------- docs/howto/loading_simulated.md | 53 +- docs/howto/marginal_checks.md | 6 +- docs/howto/postprocessing.md | 17 +- docs/howto/preprocessing.md | 6 +- docs/howto/saving_and_loading.md | 10 +- docs/howto/uncertainty.md | 6 +- docs/introduction/interfaces.md | 5 +- docs/reference/api/workflow.md | 4 +- docs/reference/features.md | 14 +- docs/reference/naming.md | 2 +- docs/tutorials/05_dispersion_tuning.md | 3 +- docs/user_guide/workflows.md | 6 +- 24 files changed, 884 insertions(+), 368 deletions(-) create mode 100644 docs/developer_guide/gui_design.md diff --git a/README.md b/README.md index 6759f14..b08e8fc 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Prefer plain pip? `python3.14 -m venv .venv && . .venv/bin/activate && pip insta The [installation guide](https://croak.readthedocs.io/en/latest/getting_started/installation.html) covers all of this in more detail. -Optional extras: `croak[gui]` (the wizard) and +Optional extras: `croak[gui]` (the graphical interface) and `croak[evo]` (the cma-es global solver; pulls in `evosax` and its `flax`/`optax` dependency tree). In a clone the same extras are `uv sync --extra gui` etc. @@ -233,9 +233,11 @@ from the underlying propagation physics. ## GUI -A PySide6 wizard (Load → Marginal check → Preprocess → Retrieve → Dispersion → -Uncertainty) drives the same library functions, with embedded plots, threaded -retrieval and a live preview: +A PySide6 window drives the same library functions. A session starts on Home +from a measured trace, a simulated scan or a synthetic pulse, and runs through +Data (load the trace and check its marginal) → Prepare → Retrieve → Analyse +(the retrieval's panels, dispersion tuning and uncertainty), with interactive +plots, threaded retrieval and a live preview: ```bash uv run croak-gui # or: uv run python -m croak.gui diff --git a/docs/developer_guide/architecture.md b/docs/developer_guide/architecture.md index 1255d4f..a3ff89c 100644 --- a/docs/developer_guide/architecture.md +++ b/docs/developer_guide/architecture.md @@ -9,13 +9,33 @@ without opening a window. | Core physics | Grids, transforms, pulse construction, interactions, propagation, focusing, and collection | [Core physics API](../reference/api/core.md) | | Retrieval | Solver selection, objectives, optimisation, progress, and result assembly | [Retrieval API](../reference/api/retrieval.md) | | Workflow | Loading, preprocessing, post-processing, uncertainty, persistence, and session replay | [Workflow API](../reference/api/workflow.md) | -| Plotting | Plot data and visualisation of traces, fields, and diagnostics | [Plotting API](../reference/api/plotting.md) | +| Plotting | Plot data, matplotlib figures, and the Qt-free screen views the GUI draws | [Plotting API](../reference/api/plotting.md) | | JAX calculations | Differentiable forward models, residuals, and pulse parameterisation | [JAX API](../reference/api/jax.md) | +| GUI | The PySide6 window: Home, the stages, worker threads, and pyqtgraph views | [GUI and session integration](gui_sessions.md) | Keep physical calculations independent of GUI state and file I/O. Prefer the shared workflow operations when adding a command or GUI control so that the same settings retain the same meaning in scripts and saved sessions. +## The GUI + +The GUI (`croak.gui`, the `gui` extra) sits on two Qt-free layers. The session +layer, `croak.session`, holds every setting as parameter dataclasses, maps each +stage onto the library, replays a saved session and writes a session's files; +the GUI, `croak replay` and `croak script` all go through it. The screen layer, +`croak.plotting.screen`, turns the data behind a figure into the arrays an +interactive view draws, using the same helpers as the matplotlib panels, so +the screen and an exported figure agree. + +On top of them, `croak.gui.window.MainWindow` takes a session from Home through +Data, Prepare, Retrieve and Analyse. The route between the stages is a set of +pure rules (`croak.gui.workflow`); each stage is a page that binds its controls +to the session's parameters and hands long computations to worker threads; and +every on-screen plot is a pyqtgraph view of screen data. Numerics belong in the +library and the two layers beneath, not in `croak.gui`. [GUI and session integration](gui_sessions.md) describes the +modules and their contracts, and [GUI design](gui_design.md) the look and layout +rules. + The [forward-model chapter](../user_guide/models.md) explains the physics; [retrieval and numerical methods](../user_guide/retrieval.md) explains the objectives and derivatives. diff --git a/docs/developer_guide/gui_design.md b/docs/developer_guide/gui_design.md new file mode 100644 index 0000000..eb1fea9 --- /dev/null +++ b/docs/developer_guide/gui_design.md @@ -0,0 +1,145 @@ +# GUI design + +This page records how croak's window is meant to look and behave, so that a new +page or control fits the ones already there. It is a living page: change it in +the same pull request as a change to the GUI's look, layout or page set. The +code it describes is `croak/gui/theme.py` (colours, type and style sheet), +`croak/gui/sheet.py` (the stage layout) and `croak/gui/window.py` (the top bar); +[GUI and session integration](gui_sessions.md) covers the modules and their +contracts, and the [GUI guide](../howto/gui.md) what a user sees. + +The window follows the look of LupoLab's solprop front end, so the two +applications read alike. + +## Principles + +- **One primary action.** Each place has one button that moves the session on, + at the right of the top bar, and it is the only button drawn in the accent + colour. + Everything else is secondary: plain buttons on the form, the ⋯ menu, the menu + bar. What the action is, and when a stage can open, are pure rules in + `croak.gui.workflow`, not decisions scattered through slots. A stage that + cannot open yet is disabled with the reason as its tooltip, never hidden. +- **Controls beside their plots.** A stage is one sheet: its settings on the + left, the plots they change on the right, and changing a setting redraws the + plots without a separate Apply where the computation is cheap enough. +- **No physics in the GUI.** `croak.gui` binds widgets to the parameters of + `croak.session` and calls the library. A calculation the GUI needs is added to + the library or to `croak.session`, where scripts and replay get it too and it + is tested without a window. +- **Screen data shared with figures.** What a plot shows is decided in + `croak.plotting.screen` from the same helpers as the matplotlib panels; the + GUI only draws it. An exported figure is the matplotlib figure of the same + data, so the screen and the saved file agree. +- **Help has one source.** A setting's help is written once, in its page's + `HELP`, and appears as its tooltip, as the form's one-line help when it has + focus, and in the page's help dialog. +- **Say it where it happens.** Whether a stage can go on is on its ready line, + at the foot of the form; a passing notice (saved, copied, why a stage is + closed) goes to the status bar, which hides when it is empty. Modal dialogs are + for choosing files and for confirming that work will be discarded. +- **Remember arrangement, not results.** The window's size, folded sections, + the read-out strip and display choices persist between launches; computed + results persist only in saved sessions. + +## Colour + +Every colour is a token of `theme.Palette`, and the one style sheet is built +from the tokens; nothing else in the GUI calls `setStyleSheet`. Add a token +rather than a literal colour. + +| Token | Value | Use | +|---|---|---| +| `ink` | `#1b2430` | text | +| `muted` | `#5a6675` | secondary text: help, units, plot tick labels | +| `heading` | `#3d4957` | small headings, unselected rail items | +| `disabled` | `#a4adb8` | disabled text | +| `axis` | `#b4bcc6` | plot axes | +| `accent` | `#0b6e99` | the primary action, focus, sliders and progress | +| `accent_hover` / `accent_off` | `#095d82` / `#b9c4cc` | the primary action under the pointer / disabled | +| `selected_bg` / `selected_ink` | `#e8f1f5` / `#0a5a7d` | the current stage, rail item or segment; links | +| `hover_bg` | `#f1f3f5` | a flat control under the pointer | +| `hairline` | `#e3e6ea` | lines between the window's regions | +| `border` | `#cdd3da` | inputs and plain buttons | +| `ok` / `warn` / `error` | `#1c7a4a` / `#8a5200` / `#b3261e` | the ready line's three states | +| `advice_bg` / `advice_border` | `#fdf8ee` / `#e6cfa8` | the amber advice box | +| `background` | `#ffffff` | everything else, plots included | + +Curves take their colours in order from `plots.COLOURS` (blue, orange, green, +purple, red, grey, ochre, cyan), each line 1.7 px wide; a curve's role +(retrieved, measured, truth, phase, …) fixes its colour and dash, as in the +figures. Maps start white at zero: viridis for positive values and, on a signed +linear map, red for negative ones. + +## Type + +The window uses IBM Plex Sans (regular, medium and semibold), bundled in +`croak/gui/assets/fonts` under the SIL Open Font License. If it fails to load, +the system font is used and a warning is logged. The scale, in logical pixels: + +| Role | Size | Weight | +|---|---|---| +| Home's heading (`h1`) | 28 | semibold | +| Page and brand headings (`h2`) | 15 | semibold | +| A derived value | 19 | medium | +| Body text, inputs, buttons | 13 | regular | +| Help, notes, plot titles, small labels | 12 | regular or medium | +| Plot ticks and axis labels | 11 | regular | + +Widgets take their style from their *role*, which is their `objectName` +(`theme.label(text, "h3")`, `theme.button(text, "primary")`, +`theme.frame("form")`), and from boolean properties such as `current`, `warn` +and `error`, which `theme.set_flag` sets and re-applies. + +## Layout + +- **Target**: 1366×768 logical pixels and up, opened maximised. +- **Top bar**: 52 px high, with a hairline beneath. Brand and context at the + left, the stage buttons in the middle, the primary action and ⋯ at the right. +- **Sheet**: the form (at least 360 px) and the plots share the width 2 : 3. + Below a window width of 1000 px the plots move under the form. The form + scrolls on its own; its help line and ready line stay at its foot. +- **Sections** fold in place. Settings most sessions leave alone start folded + and remember their state (a `settings_key`), as Retrieve's advanced sections + do. +- **Plots** are white, closed boxes (top and right axes drawn without numbers), + with a crosshair and readout on hover, mouse zoom and pan, and a double-click + back to the starting view. Plots that share an axis are linked. A plot is at + least 140×100 px. +- **Strips** beneath the plots that have a natural height (Retrieve's numeric + read-out) fold away and hand their height back to the plots. +- **Analyse** has a 170 px rail of pages on the left. +- **Drawer**: 440 px on the right, for text the user may copy (the settings, + the script). + +## Page catalogue + +| Place | Page | Primary action | Plots | +|---|---|---|---| +| Home | tiles, Open a session…, recent sessions | none | none | +| Data | Load the trace / Load the scan / Generate the trace | Continue | the loaded trace; for a scan its maps and marginals; for a synthetic pulse the pulse, spectrum, spectrogram and trace | +| Data | Check the marginal | Continue | the trace and the marginal comparison | +| Prepare | — | Continue | measured, filtered and regridded traces in dB and linear (2×3) | +| Retrieve | — | Retrieve / Stop / Retrieve again | the live overview, or convergence before the first preview; the numeric read-out | +| Analyse | Overview, Traces, Pulse, Diagnostics | Save | the retrieval's panels, two by two | +| Analyse | Dispersion | Save | dispersed spectrum, pulse, applied phase, spectrogram | +| Analyse | Uncertainty | Estimate / Stop | FWHM distribution (or FWHM and error against thickness), the temporal band | + +## Left for later + +These are known gaps, kept out of the redesign to keep it reviewable: + +- **A dark theme.** The style sheet is built from one `Palette`, so a dark one + would be a second instance, but the plots' white background and colour maps + would need choosing too. +- **The unused sheet parts.** `sheet.AdviceBox` (an amber box saying what went + wrong and what to do), `DerivedGrid` (numbers derived from the settings) and + `ProgressStrip` (state, bar and figures over the plots while work runs) are + built but no stage uses them yet; a run's progress is on the ready line. +- **The stage pages' interface.** The stage modules still build through + `base.Stage`, which keeps the calls they were written against for the old + wizard; moving them onto `StagePage` directly is a later clean-up. +- **Find a setting** is offered on Retrieve, the longest form, only. +- **Two ways to save a retrieval.** Retrieve's own **Save…** writes the same + three files as **Save session…**, without the uncertainty estimates and + without adding the session to Recent sessions. diff --git a/docs/developer_guide/gui_sessions.md b/docs/developer_guide/gui_sessions.md index f1206db..1e63c79 100644 --- a/docs/developer_guide/gui_sessions.md +++ b/docs/developer_guide/gui_sessions.md @@ -13,28 +13,151 @@ The [session API](../reference/api/workflow.md#sessions-headless-replay) documen the serialisable settings and execution interfaces. The [command-line and scripting API](../reference/api/workflow.md#command-line-and-script-generation) covers headless use and generated scripts. User-facing instructions belong in -[saving, sessions, and reproducible workflows](../user_guide/workflows.md). - -## Live views (GUI support) - -Two classes redraw nothing when new data arrive: they draw a figure once and then -push later data onto the existing artists, which is what keeps the wizard's live -previews cheap and steady. {class}`~croak.plotting.FrogFilterView` does this for -the six-panel filter figure (`attach(fig)` then `update(td)`), and -{class}`~croak.plotting.RetrievalPageView` for one page of retrieval panels -(`draw(fig, data)`, then `update(data)` whenever `can_update(data)` says the new -result differs only in values — same grid, same optional curves). Behind the page -view, every `draw_` has an `update_(artists, data)` twin, and the -invariant the tests hold them to is that `update(draw(d1), d2)` shows exactly -what `draw(d2)` shows. {func}`~croak.plotting.plot_convergence` returns its -artists for the same reason, and `update_convergence_curve` grows the error -history in place. - -An embedded `MplCanvas` keeps its owning Qt window alive for the duration of a -draw. A queued Matplotlib draw holds the Python canvas wrapper, but that alone -does not keep Qt from deleting the canvas when its parent is garbage-collected. -Retaining the window during rendering prevents a discarded stage from losing -its canvas midway through a draw; it does not retain the window after drawing. +[saving, sessions, and reproducible workflows](../user_guide/workflows.md) and +the [GUI guide](../howto/gui.md); the look and layout rules are in +[GUI design](gui_design.md). + +## Three layers + +The GUI is the thinnest of three layers, and only it imports Qt: + +- **`croak.session`** — the per-stage parameter dataclasses + (`croak.session.params`), the serialisable `SessionOptions`, the mapping of + each stage onto the library (`croak.session.pipeline`), headless replay + (`croak.session.engine`), retargeting (`croak.session.retarget`) and a saved + session's files (`croak.session.files`: `save_session` writes `result.h5`, + `options.toml` and `retrieval.pdf`; `result_mismatch` says whether a saved + result was fitted to the trace a replay produced). +- **`croak.plotting.screen`** — what an on-screen view shows, as plain data: + `MapView` for an image and `LineView` for curves, built by functions such as + `trace_map`, `temporal_view`, `filter_views` and `uncertainty_views`. They use + the same helpers as the matplotlib panels, so a curve on screen is the curve in + an exported figure. +- **`croak.gui`** — Qt glue: widgets bound to the session's parameters, worker + threads, and pyqtgraph views that draw the screen data. + +Importing `croak` or `croak.gui` does not import Qt; {func}`croak.gui.run_gui` +does. A change that needs numerics belongs in the first two layers, where it is +tested without a window. + +## The window + +| Module | Holds | +|---|---| +| `window` | `MainWindow`: top bar, stage stack, drawer, menus, and the session actions (new, open, replay, retarget, save) | +| `workflow` | the route as pure rules: `Stage`, `Progress`, `blocked_reason`, `next_stage`, `primary_action`, `remember_recent` | +| `home` | Home: the entry tiles, Open a session… and the recent sessions | +| `pages` | `default_pages` (the page factory), `DataPage` (loader and marginal check as two steps) and `AnalysePage` (the rail) | +| `stage` | `PageBase`, the hooks the window reads, and `StagePage`, a page that is a sheet | +| `base` | `Stage`, the interface the stage modules are written against (`controls`, `set_plot_area`, `set_status`, `advance`), and `RunControls` | +| `sheet` | the sheet's parts: `Form`, `Section`, `ReadyLine`, `Segments`, `AdviceBox`, `DerivedGrid`, `ProgressStrip` and `Sheet` itself | +| `stage_load`, `stage_simulated`, `stage_synthetic` | the three loaders (Data's first step) | +| `stage_marginal_check`, `stage_preprocess`, `stage_retrieve`, `stage_dispersion`, `stage_uncertainty` | the other stage pages | +| `retrieval_views` | the retrieval's panels on screen (`RetrievalPanels`) and Analyse's result pages (`ResultPage`, with Export and Pop out) | +| `plots` | the pyqtgraph views: `Plot`, `MapPlot`, `LinePlot`, the colour maps and the hover readers | +| `theme` | the palette, type scale, style sheet and the role helpers (`label`, `button`, `frame`, `set_flag`) | +| `controls`, `widgets`, `readout`, `mirror_stack` | form builders, slider/spin-box composites, Retrieve's numeric read-out, Dispersion's mirror stack | +| `help` | per-stage help: the dialog and the tooltips, from one `HELP` list | +| `state` | `WizardState`: the parameters and pipeline outputs, with cascading invalidation | +| `worker` | the `QThread` workers for preprocessing, retrieval and uncertainty | +| `settings` | remembered preferences in a `QSettings` store | +| `branding` | the application's name and icon | + +### The route + +Which stage can open and what the primary action does depend only on +`workflow.Progress`, a frozen snapshot of what the session holds. The rules are +pure functions with doctests and their own tests (`tests/gui/test_workflow.py`); +`MainWindow.refresh` re-applies them to the top bar whenever the state or a page +says something changed. Put a new navigation rule there, not in a widget's slot. + +### Pages + +A page tells the window what it can do through the `PageBase` hooks: +`on_enter`, `can_advance`, `busy`, `page_key` (which sub-page is shown, for Data +and Analyse), `act(kind)` for the primary action, `overview` (the retrieval +behind a saved session's PDF) and `show_help`. It emits `changed` when the +answers change and `message` for a status-bar notice. + +`MainWindow` builds a session's four pages with a factory +(`pages.default_pages`), and rebuilds them for every new or reopened session, +because their controls bind to the parameter objects on the shared +`WizardState`. A test can pass its own factory to `MainWindow`. + +Most stage pages subclass `base.Stage`, a `StagePage` that keeps the calls the +stage modules were first written against: sections go into `self.controls` +(from `controls.group(title, folded=…, settings_key=…)`), plots into +`set_plot_area(plot, *strips)`, and messages onto the ready line with +`set_status(text, "ok" | "warn" | "error")`. A loader implements `advance` and +emits `advanced("check")` or `advanced("retrieve")`; `DataPage` calls it on +Continue. A page that runs a worker mixes in `RunControls`, whose `busy` turns +the primary action into Stop. + +Help is declared once per page as `HELP = [(section, [(label, text), ...])]`. +`apply_help` attaches each text as the tooltip of the row with that +`(section title, row label)`, the form shows the focused control's tooltip as +its one-line help, and `HelpDialog` lists them all. A row label spells a literal +ampersand `&&` (Qt's mnemonic escape); `HELP` keys are written as the user reads +them. + +## Screen views (GUI support) + +What a view shows is decided in `croak.plotting.screen`; how it is drawn is +decided in `croak.gui.plots`. The screen functions take the same inputs as the +matplotlib panels — for the retrieval, a `RetrievalPlotData` from +{func}`~croak.plotting.retrieval_plot_data` — and return arrays, labels, ranges +and curve roles. Two display choices differ from the figures: an image is +resampled onto evenly spaced axes at its finest spacing (an interactive image is +placed by its corners, a `pcolormesh` by every edge), and a logarithmic trace +shows only the positive part, leaving negative samples to the signed linear maps. + +`retrieval_views.PANELS` maps each key of +{data}`~croak.plotting.RETRIEVAL_PANELS` to its screen function, so the GUI's +pages and the exported figures share the panel names and layouts +({data}`~croak.plotting.RETRIEVAL_PAGES`). A live retrieval calls +`RetrievalPanels.set_data` on the plots it already has, which replaces their data +without rebuilding the widgets. Exported figures — **Export ▾ → Figure…**, the +pages' **Save…** buttons, a saved session's `retrieval.pdf` — go through the +matplotlib `draw_*` and `plot_*` functions. + +The screen functions are Qt-free and tested in `tests/test_plotting_screen.py`; +a new view starts there. + +## Threads + +The workers in `croak.gui.worker` add only threading, progress signals and +cancellation to the stage operations in `croak.session.pipeline`, so the GUI and +the headless engine run identical computations. `RetrievalWorker` emits live +previews at most every `PREVIEW_MIN_INTERVAL` (0.25 s). Retrieve reports what +presenting each one cost, and the worker stretches the interval to +`cost / PREVIEW_DUTY` (a duty of 0.25), so presenting never takes more than about +a quarter of the wall-clock time on a slow machine. + +## Remembered preferences + +`croak.gui.settings` keeps the window geometry, each folding section's state +(sections with a `settings_key`), the read-out strip, Retrieve's spectrum axis +and the recent sessions in a `QSettings` INI store, opening a short-lived store +for every read or write. It always names the organisation and application +explicitly, which is what lets the test suite redirect the store to a per-test +directory with `QSettings.setPath` (`tests/conftest.py`). + +## Testing the GUI + +The GUI tests use pytest-qt with `qt_api = "pyside6"` and run offscreen +(`QT_QPA_PLATFORM=offscreen`, set in `tests/conftest.py` and the pytest +environment), driving the real widgets through `qtbot`. They count towards the +100 % coverage gate like everything else. + +Many stage tests were written for the old six-stage wizard. +`tests/gui/gui_session.py` keeps them working: its `Driver` builds a +`MainWindow`, maps the old stage numbers onto the window (1 the loader and 2 the +marginal check on Data, 3 Prepare, 4 Retrieve, 5 Dispersion and 6 Uncertainty on +Analyse) and hands back the stage pages, which keep their behaviour. `goto` +bypasses the route rules, as the old stage setter did, so a test can look at a +page before its input exists. Tests of navigation itself use the window and the +route rules directly (`test_window.py`, `test_window_session.py`, +`test_workflow.py`). When testing saved window geometry, expose both the original and restored window with `qtbot.waitExposed` before comparing their displayed sizes. Qt diff --git a/docs/developer_guide/index.md b/docs/developer_guide/index.md index 5b16279..8ec39e0 100644 --- a/docs/developer_guide/index.md +++ b/docs/developer_guide/index.md @@ -18,6 +18,7 @@ environment architecture extending gui_sessions +gui_design validation documentation releasing diff --git a/docs/examples/thickness_uncertainty.md b/docs/examples/thickness_uncertainty.md index dec84ca..0f425f6 100644 --- a/docs/examples/thickness_uncertainty.md +++ b/docs/examples/thickness_uncertainty.md @@ -118,7 +118,7 @@ The central thickness is `options.retrieve.thickness_um`, and the retrieval must be dispersive. To call {func}`croak.thickness_bootstrap` directly, pass it `session.result` and `session.tracedata.trace`, with the material, quadrature `npoints` and `algorithm` the retrieval used. In the GUI the same estimate is -stage 5 with method "thickness". See [saving and loading](../howto/saving_and_loading.md). +the Uncertainty page under Analyse, with method "thickness". See [saving and loading](../howto/saving_and_loading.md). ### Limits diff --git a/docs/explanation/uncertainty_estimation.md b/docs/explanation/uncertainty_estimation.md index f8b2d8a..d292ef2 100644 --- a/docs/explanation/uncertainty_estimation.md +++ b/docs/explanation/uncertainty_estimation.md @@ -365,8 +365,8 @@ What this does and does not capture: optics' own uncertainty. (A `thickness_bootstrap`-style systematic on the propagation optics is the natural extension.) -In the GUI (stage 5) tick *"at dispersion-stage point"* to evaluate the bar at the beamline -point configured in the Dispersion stage. +In the GUI (the Uncertainty page under Analyse) tick *"at dispersion-stage point"* to +evaluate the bar at the beamline point configured on the Dispersion page. ## The noise model diff --git a/docs/getting_started/gui.md b/docs/getting_started/gui.md index bc00ea0..9f9aadf 100644 --- a/docs/getting_started/gui.md +++ b/docs/getting_started/gui.md @@ -3,7 +3,7 @@ Install the graphical interface to inspect and retrieve a FROG trace without writing Python. [uv](https://docs.astral.sh/uv/) supplies Python and creates an isolated environment for croak. You will use a terminal for installation and -launching; subsequent analysis takes place in the wizard. +launching; subsequent analysis takes place in the GUI's window. ## Before you start @@ -56,29 +56,56 @@ configuration. If the window fails to appear on Windows, run output in the terminal. The window opens maximised and is laid out for displays of 1366×768 and up; -un-maximised, it returns to the size you last left it. +un-maximised, it returns to the size you last left it. It opens on **Home**: -```{image} ../_static/gui_retrieve.png -:alt: The wizard after a retrieval — the Traces page (measured and retrieved traces, log and linear) with the Pulse and Diagnostics tabs beside it, and the numeric read-out beneath. +```{image} ../_static/gui_home.png +:alt: Home — three tiles to start from a measured trace, a simulated scan or a synthetic trace, an "Open a session…" button, and the list of recent sessions on the right. :width: 95% :align: center ``` ## 4. Use it -The wizard walks through the retrieval in stages: **Load** (your measured trace -from an HDF5 or `.npz` file, with an optional independent spectrum from HDF5, -`.npz` or CSV and calibration curves from CSV; or a simulated scan), -**Marginal check**, **Preprocess** (filtering, background removal, regridding), -**Retrieve**, **Dispersion** (tuning with materials and chirped mirrors) and -**Uncertainty** (error bars on the retrieved duration). Hover over a control for -help with that setting, and use each stage's **? Help** button for guidance. - -The [GUI guide](../howto/gui.md) documents each stage in detail. Sessions save -to a TOML file that records every setting, so a retrieval can be re-run later -— including headlessly with `croak replay session.toml` — or exported as a -standalone Python script with `croak script session.toml` (the `croak` command -is installed alongside `croak-gui`). +Pick a tile on Home — **Measured trace** (an HDF5 or `.npz` file, with an +optional independent spectrum from HDF5, `.npz` or CSV and calibration curves +from CSV), **Simulated scan** or **Synthetic trace** (a pulse you specify, to try +croak without data). The session then runs through four stages, shown as +buttons in the top bar: + +- **Data** — load (or generate) the trace, then check its frequency marginal + against the independent spectrum; +- **Prepare** — filtering, background removal, windowing and regridding; +- **Retrieve** — choose a solver and run it, with a live preview; +- **Analyse** — the retrieval's pages of panels, then **Dispersion** (tuning with + materials and chirped mirrors) and **Uncertainty** (error bars on the + retrieved duration). + +The button at the right of the top bar does the next thing — **Continue**, +**Retrieve**, **Save** — and a stage you cannot open yet is greyed out, with the +reason in its tooltip. Settings are on the left of each page and plots on the +right; hover over a control for help with that setting, and over a plot to read +its values. Scroll to zoom, drag to pan, and double-click to return to the +starting view. + +```{image} ../_static/gui_retrieve.png +:alt: Retrieve after a run — the solver settings on the left, the measured and retrieved traces above the retrieved pulse and spectrum on the right, and the numeric read-out beneath. +:width: 95% +:align: center +``` + +```{image} ../_static/gui_analyse.png +:alt: Analyse — a rail of pages (Overview, Traces, Pulse, Diagnostics, Dispersion, Uncertainty) beside the Pulse page: the retrieved pulse and spectrum above the delay and frequency marginals, with Save as the primary action. +:width: 95% +:align: center +``` + +The [GUI guide](../howto/gui.md) documents each stage in detail. **Save +session…** in the ⋯ menu writes the retrieval, a TOML file that records every +setting and an overview PDF into a folder, and the session then appears under +Recent sessions on Home. A saved session can be re-run later — including +headlessly with `croak replay options.toml` — or exported as a standalone Python +script with `croak script options.toml` (the `croak` command is installed +alongside `croak-gui`). ## Updating diff --git a/docs/howto/collection_aperture.md b/docs/howto/collection_aperture.md index aca2023..ca3c952 100644 --- a/docs/howto/collection_aperture.md +++ b/docs/howto/collection_aperture.md @@ -95,7 +95,8 @@ hard-edged pinhole at the phase-matched corner) and `spectrum_frame_p` (the spectral-frame exponent — the mixture's field is the *on-axis* one, bluer than an integrated measured spectrum; the exponent reweights the initial guess and the `reg_spectrum` target together into that frame). The GUI exposes all of -these in the *Chromatic focal mixture (advanced)* group of the retrieve stage, +these in the *Chromatic focal mixture (advanced)* section of the Retrieve stage +(folded by default), mutually exclusive with the reduced-kernel checkbox, and `croak.session.pipeline.focal_mixture_from_params(params, td, scan_path)` builds the object — `run_retrieval` calls it automatically when `params.focal` diff --git a/docs/howto/fitting_thickness_tau0.md b/docs/howto/fitting_thickness_tau0.md index c405e6c..e0846cb 100644 --- a/docs/howto/fitting_thickness_tau0.md +++ b/docs/howto/fitting_thickness_tau0.md @@ -90,7 +90,7 @@ fit the extras from a **good pulse estimate**, not a cold start. Two ways: than restarting from the priors (a spectrum that was retrieved jointly with a non-zero $\tau_0$ is otherwise re-fitted against a differently-referenced delay axis). The GUI does this for you: *Reuse previous result* seeds every - extra that is **still being fitted**, and says which in the status line. An + extra that is **still being fitted**, and says which on the ready line. An extra whose fit box you have unticked keeps the value you set — an unticked box means "hold this where I put it", and the model is evaluated there. @@ -142,7 +142,8 @@ compare with repeated fits or a suitable bootstrap. ## In the GUI -The retrieve stage has an **Extra fit parameters** group with *Fit medium +The Retrieve stage has an **Extra fit parameters** section (folded by default; +click its title to open it) with *Fit medium thickness*, *Fit τ₀ delay offset*, *Fit smearing width* and *Two-phase polish*. *Fit smearing width* is enabled only when *Enable geometrical smearing* is on (see [Geometric smearing](geometric_smearing.md)). *Fit medium thickness* diff --git a/docs/howto/geometric_smearing.md b/docs/howto/geometric_smearing.md index 4556178..9e5770d 100644 --- a/docs/howto/geometric_smearing.md +++ b/docs/howto/geometric_smearing.md @@ -265,7 +265,7 @@ chosen solver supports it: - *Quadrature nodes* *Fit smearing width* sits in **Extra fit parameters** and is enabled only once the -kernel is on. When it has been fitted, the post-retrieval status line reports the +kernel is on. When it has been fitted, the ready line under the form reports the result next to the other fitted extras, as the physical delay width and the multiplier it came from: diff --git a/docs/howto/gui.md b/docs/howto/gui.md index 2cb9121..5b1ee90 100644 --- a/docs/howto/gui.md +++ b/docs/howto/gui.md @@ -1,15 +1,14 @@ -# The wizard GUI +# Using the GUI -The graphical wizard guides you from a loaded trace to a retrieved pulse, -dispersion analysis and uncertainty estimates. Its controls use the same -calculations as the Python interface. If this is your first visit, follow +The graphical interface takes you from a loaded trace to a retrieved pulse, +dispersion analysis and uncertainty estimates in one window. Its controls use the +same calculations as the Python interface. If this is your first visit, follow [installing the GUI](../getting_started/gui.md), then try a synthetic trace whose pulse is known. -Every plot carries the standard matplotlib navigation toolbar (Home, Pan, -Zoom-to-rectangle, Save, and a live cursor read-out), so you can zoom and pan to -inspect a trace or marginal. A full redraw (changing a control, recomputing) -resets the view; **Home** restores it. +A session runs through four stages — **Data → Prepare → Retrieve → Analyse** — +which you reach from **Home**. The top bar always shows where you are, which +stages you can open and the one action that moves the session on. ## Launching @@ -30,23 +29,33 @@ croak.gui.run_gui() {func}`croak.gui.main` is the console-script entry point; {func}`~croak.gui.run_gui` creates the `QApplication` and shows the window -**maximised**, because the plot pages need the room. Un-maximising gives the -window the size you last left it (remembered between launches and clamped to the -screen it opens on), or on a first run a window filling 90 % of the screen. - -The wizard is laid out for displays of **1366×768 logical pixels and up** (a -1920×1080 monitor at Windows' 125 % scaling is 1536×864 logical pixels). Plot -text (titles, axis labels, tick labels, legends and their padding) shrinks as a -canvas gets smaller than the size its figure was designed for, down to 70 % of -the default size, and grows back when the window does. A laptop therefore shows -the same panels at a smaller type size rather than losing the axes behind their -labels. Figures written by a stage's **Save…** button are drawn afresh at full -size, whatever the window size ({class}`~croak.plotting.PlotScale` is the -mechanism). +({class}`croak.gui.MainWindow`) **maximised**, because the plots need the room. +Un-maximising gives the window the size you last left it (remembered between +launches and clamped to the screen it opens on), or on a first run a window +filling 90 % of the screen. + +The GUI is built on PySide6. Every plot on screen is an interactive pyqtgraph +view; figures you export or save are drawn by matplotlib ({mod}`croak.plotting`), +so they look the same as the figures a script makes. + +### Small screens + +The window is laid out for displays of **1366×768 logical pixels and up** (a +1920×1080 monitor at Windows' 125 % scaling is 1536×864 logical pixels). Each +stage puts its settings on the left and its plots on the right; when the window +is narrower than 1000 pixels the plots move below the settings instead. The +settings are grouped in sections that fold, and Retrieve's +advanced sections start folded, so a laptop screen shows the settings you use +most without scrolling. The numeric read-out beneath Retrieve's plots folds away +with a click on its header, and returns the height to the plots. + +Plot text stays at the same size whatever the window size. Figures written by +**Export ▾ → Figure…** or by a **Save** are drawn afresh at their own design +size, so they do not depend on the window either. ### Desktop integration -The wizard carries its own icon (`croak/gui/resources/croak.svg`) and identifies +The GUI carries its own icon (`croak/gui/resources/croak.svg`) and identifies itself as **croak** rather than as the Python interpreter running it: that is the icon in the macOS Dock, the Windows taskbar and the Linux dock, and the name in the macOS menu bar. Two platform details remain: @@ -58,249 +67,426 @@ the macOS menu bar. Two platform details remain: console window, but a crash before the window appears is then silent. Run `uv run python -m croak.gui` from a terminal to see the traceback. -## Navigation - -Every page except the Welcome menu carries the same controls: a header bar with the -page title and a **? Help** button that explains that page's options, and a -footer with **Cancel** and **Main menu** at the bottom left and **← Back** / -**Next →** at the bottom **right**. That includes the two entry pages, where the -forward button does the page's own work and is labelled accordingly — -**Generate →** on the synthetic generator, **Load & continue →** (or -**Load raw → Retrieve →**) on the simulated loader — and is disabled until the -page has something to hand on. **Back** from either entry page returns to the -Welcome menu, as does **Back** on Stage 1 and the **Main menu** button. If the -session already holds a loaded trace or a retrieval, the wizard asks first: every -menu entry starts a new session, so choosing one would discard them. - -### Layout - -Each stage keeps its controls in a scrollable column on the left and its plots on -the right, separated by a draggable divider. Drag it to trade control space for -plot space, or drag it fully to the left to hide the controls altogether; the width -you choose applies to every stage and is remembered for the next launch. A strip -beneath a plot with a titled header (the Retrieve stage's numeric read-out) folds -away with a click on the header, and that is remembered too. +## Home + +Home is where a session starts. It offers three tiles, one for each kind of +data: + +- **Measured trace** — load a FROG measurement from HDF5 or NPZ, with its + independent spectrum, backgrounds and calibration curves. +- **Simulated scan** — load a numerically simulated scan (a `scansave` file), + with its known input pulse and instrument geometry. +- **Synthetic trace** — generate a trace from a pulse you specify, with + dispersion and noise, and test a retrieval against the known answer. + +Beneath the tiles, **Open a session…** reopens a saved session from its +`options.toml` (see [Sessions](#sessions)). The **Recent sessions** list on the +right holds the last six sessions you saved or opened, named by their folder, +with the folder's location and the time the session was last saved; a session +whose file has gone is left out. Click one to reopen it. + +Starting a new session, or opening one, replaces the session in the window. If +that session holds a trace or a retrieval, the GUI asks first, with **Discard** +or **Cancel**; **Save session…** in the ⋯ menu keeps it. + +## The top bar + +The top bar carries, from left to right: + +- the **croak** brand, which returns to Home. The session stays open: its stage + buttons are still there, so you can go back to it; +- a context label: what the session is (*Measured trace*, *Simulated scan* or + *Synthetic trace*) and, once it has been saved or opened, its folder's name; +- the four stage buttons **Data · Prepare · Retrieve · Analyse**. A stage that + cannot be opened yet is greyed out, and its tooltip says why; +- the **primary action**, the one button that moves the session on; +- the **⋯** menu: **Save session…**, **Open a session…**, **Show the settings + (options.toml)**, **Show as a Python script** and **Help for this stage**. + +The menu bar repeats these under **File** (Home, Open a session… and Save +session…, with the platform's Open and Save shortcuts), **View** (the settings +and the script) and **Help** (Help for this stage, F1 on Windows and Linux, ⌘? +on macOS). Passing notices, such as "Saved …" or why a stage cannot open, +appear in a status bar at the foot of the window, which hides again when there +is nothing to say. + +### The route + +Which stages can be opened depends only on what the session holds: + +| Stage | Opens once | +|---|---| +| Data | a session has started | +| Prepare | a trace has been loaded | +| Retrieve | the trace has been prepared | +| Analyse | a retrieval has run | + +A simulated scan loaded with **Skip filtering & regrid** is retrieved on its own +grid, so it goes from Data straight to Retrieve and Prepare stays disabled. +Changing a setting upstream clears what was derived from it — a new trace clears +the prepared trace and the retrieval — so the GUI never shows a pulse retrieved +from superseded settings, and the later stages close again until they are +recomputed. + +The primary action follows the stage: + +| Where | Primary action | +|---|---| +| Data | **Continue**: from the loader to the marginal check, then to Prepare | +| Prepare | **Continue** to Retrieve | +| Retrieve | **Retrieve**, **Stop** while it runs, then **Retrieve again** | +| Analyse | **Save** (the session); on the Uncertainty page **Estimate**, or **Stop** while it runs | + +The action is greyed out until it can be done: Continue on Data, for example, +until there is a trace to hand on. + +## The stage pages + +Every stage page has the same layout. On the left is a form of **sections**, +each headed by a title you click to fold or unfold it. Hover over a control for +its help; when a control has the keyboard focus, the same help is shown as one +line under the form. **Help for this stage** (⋯ menu or F1) opens a summary of +every setting on the page. The Retrieve form also has a **Find a setting** box, +which shows only the rows whose label or help contains what you type; typing +part of a section's title shows that whole section. + +At the foot of the form a **ready line** says whether the stage can go on: green +when it can, amber with a warning, red when something has to be fixed first. On +the right are the stage's plots. + +## Plots + +Every plot shares the same handling: + +- **Hover** to show a crosshair and a read-out: the position, and the value + of the map or of each curve under the pointer. +- **Scroll** to zoom about the pointer, and **drag** to pan. +- **Double-click** to return to the view the data were first shown with. + +Plots that share an axis are linked, so zooming one zooms its partners: the +trace maps on Prepare, and the measured and retrieved traces on Retrieve and +Analyse. Images are shown on evenly spaced axes at their finest sample spacing, +so no detail is lost where the samples are densest. A logarithmic (dB) trace +shows the positive part of the signal; negative samples, left by background +subtraction, show on the linear maps, which run from white to viridis for +positive values and from white to red for negative ones. + +The pages on Analyse also have **Export ▾**: **Figure…** saves that page as a +PDF, PNG or SVG drawn by matplotlib, and **Pop out** opens it in a window of its +own, which follows later results. ## The stages -The wizard is a stacked sequence of pages with shared navigation. The six linear -stages — **Load → Marginal check → Preprocess → Retrieve → Dispersion → -Uncertainty** — sit behind three entry pages (Welcome, Synthetic, Simulated); the -synthetic generator and the simulated loader are each their session's "stage 1" -and hand the trace straight to the marginal-check stage. - -1. **Welcome** — choose to load a measured file, load a numerically simulated - scan, load a previous retrieval, or generate a synthetic trace. -2. **Synthetic** *(optional)* — build a test trace (pulse shape, interaction, - delays, optional dispersive slab) using {func}`~croak.forward.maketrace`. Useful - for trying the workflow without data. The **duration** is the intensity - (power) FWHM of the temporal envelope — both Gaussian and sech² honour this - convention ({func}`~croak.pulses.gaussian_pulse`, - {func}`~croak.pulses.sech_pulse`). The delay and wavelength samplings each - expose an interlocked **count** and **step** at fixed range (editing one - updates the other), and a **Spectrum (independent)** group generates a - fundamental spectrum that is passed downstream (range / step / # points); by - default it **mirrors the FROG-trace λ window**, scaled to the fundamental - (×2 for SHG). With it present, the marginal-check overlay and the SHG marginal - correction work on synthetic traces too. The known pulse (its temporal - intensity envelope `It`) is carried through and shown beneath the retrieved - pulse, and saved with the result. -3. **Simulated** *(optional)* — load a numerically simulated `scansave` scan and - preview it, then hand it to the marginal-check stage. A **thickness** dropdown - selects which saved propagation depth to retrieve from a multi-thickness scan - (disabled for older single-thickness files). See - [Loading numerically simulated traces](loading_simulated.md). -4. **Load** — pick an HDF5/NPZ file and the trace/wavelength/delay datasets, with - unit and axis auto-detection ({mod}`croak.io`). An optional independent - spectrum can come from HDF5, NPZ or CSV, with background subtraction, and - wavelength calibration curves from CSV. **Transpose** fixes a trace - that loaded as (delay × wavelength), and **Reverse delay axis** flips the - delay-sign convention — negating the scan axis, so the trace is mirrored about - τ = 0. Which end of a stage scan is "gate late" is a wiring convention the - file does not record. It matters for **PG and SD**, whose kernels fix the - direction of time: a wrong setting time-reverses the retrieved pulse and flips - the sign of every phase order. The error is invisible on a symmetric - transform-limited pulse, so check the setting once against a pulse of known - chirp (for example after adding a known glass thickness). An **SHG** trace - is symmetric in delay even for a chirped pulse, so reversing it changes - nothing and cannot settle SHG's own - [direction-of-time ambiguity](../explanation/pnps_framework.md#ambiguities) — - that is resolved after retrieval instead - ([SHG: settle the direction of time first](postprocessing.md#shg-settle-the-direction-of-time-first)). - It is the measured counterpart of the simulated loader's switch of the same - name. - Optionally enter the measured - **pulse energy** (J) here to rescale the temporal plots to absolute power (see - the Dispersion stage). The trace preview uses the same colour scheme as the - preprocessing stage (white→viridis for positive, white→red for negative), with - the display scaled to the in-trust-band signal so an aggressive efficiency tilt - cannot wash the image out. -5. **Marginal check** — compare the trace's frequency marginal with the marginal - **predicted** from the independent spectrum ({mod}`croak.marginal_checks`): - exact for SHG (the spectral autoconvolution) and transform-limited for - PG/TG/SD, within the thin local model; the [marginal guide](marginal_checks.md) - states its assumptions. It overlays the predicted marginal, marks the predicted vs measured **centroid**, - reports the **width-bracket** check, and exposes the spectral **trust region** - (defaults to 190–1000 nm for a measured trace, or the full loaded range for - the noise-free simulated/synthetic traces), a **frequency/wavelength** axis - toggle (default frequency, the space the retrieval works in; the wavelength view - applies the $1/\lambda^2$ density Jacobian, which shifts a broad marginal towards - the blue), a **log/linear** - marginal toggle (default linear, - peak-normalised) and the **3rd-order** `(λ/µm)^exp` efficiency correction. Its - **Auto** button picks the exponent whose corrected-marginal centroid matches the - prediction ({func}`~croak.marginal_checks.auto_third_order_exponent`). The - **SHG marginal correction** is available here: it - scales the trace's frequency rows onto the predicted SHG autoconvolution - ({func}`~croak.marginal_checks.apply_marginal_correction`), is enabled only for - an SHG trace with an independent spectrum, and previews the snap onto the - prediction live (the correction itself is applied during Preprocess). All three - entry paths (experimental, simulated, synthetic) pass through this stage; see - [Marginal consistency checks](marginal_checks.md). The trace and the marginal - comparison share a draggable divider, so either plot can be given the height. -6. **Preprocess** — fringe/DC/low-pass filtering, windowing and regridding - ({func}`~croak.preprocess.load_and_clean`), with a live before/after preview. - The controls always show the settings that were actually applied: re-entering - the stage re-seeds every widget from the current parameters, and those - parameters survive a round trip back to the loader as long as the trace is - unchanged (see [Preprocessing](preprocessing.md#settings-across-a-re-load)). - With **Baseline (arPLS)** on, the **Auto** button beside *τ exclude (fs)* sets the - hold-out to twice the loaded trace's delay-marginal FWHM. Use it on measured - traces: without a hold-out the baseline bows into the τ ≈ 0 peak and removes - signal, worst on the rows that constrain the fit least. The failure shows up - as a thin stripe at one wavelength in the cleaned trace (see - [Peak suck-up](preprocessing.md#peak-suck-up-and-the-hold-out)). -7. **Retrieve** — choose the algorithm (any of the ten, including the global - [`cma-es`](../explanation/algorithms.md#global-retrieval-cma-es)) and its - parameters and run. Retrieval runs in a background thread, driven by the - solver `callback`, so the interface stays responsive. The result is shown on three - **pages** of panels, as tabs: **Traces** (measured and retrieved trace, log and - linear, with their colour bars), **Pulse** (retrieved pulse over the delay marginal, spectrum over the - frequency marginal) and **Diagnostics** (convergence, residuals, spectral - filter, spectrogram) — the same twelve panels as - {func}`~croak.plotting.plot_retrieval`, two by two so each has room even on a - laptop display. **Pop out** (top right of the tabs) opens the current page in - its own window, which follows later results; each tab's toolbar **Save** writes - that page, and the stage's **Save…** writes the full twelve-panel overview as - `retrieval.pdf` beside `result.h5` and `options.toml`. The **Display** group's - **Spectrum axis** draws the spectrum panel against **frequency** (PHz, the - default — the space the retrieval works in) or **wavelength** (nm, the - spectrometer's axis, carrying the $1/\lambda^2$ density Jacobian); both are unit-peak densities - in their own variable, the choice is remembered, and the exported PDF follows - it. - - By default **Live full-plot preview** redraws the visible page as the run - converges (other pages redraw when selected). Previews arrive at most four - times a second, and less often on a machine where presenting one is slow: the - wizard measures what each preview costs and stretches the interval so that - presenting stays below about a quarter of the wall-clock time, so the preview - never starves the solver. Until the first preview arrives, the convergence - curve is shown on the visible page, redrawn at up to ten frames a second - however fast the solver iterates. A page is laid out once per run and later - snapshots update its existing curves, images and labels, so nothing shifts - between frames; the spectrogram, the costliest panel, is computed only when - the Diagnostics page is drawn. - Pressing **Stop** halts the run at that point and leaves the latest full plot - on screen as a (non-converged) result you can inspect, save or post-filter. - Untick **Live full-plot preview** to fall back to the cheaper - trace-error-vs-iteration curve (the full plot is still drawn once the run - finishes). Every solver supports the preview, including `copra`/`copra-jax` - and the COPRA warm-up of `warm-lbfgs`, which is 300 iterations by default. - `warm-lbfgs` plots **both** stages on one convergence curve - with a dashed rule at the handover; read the two sides as different units of - work (COPRA iterations, then L-BFGS function evaluations). The **Initial - guess** selector makes automatic, transform-limited, random, perfect-Gaussian, - known **truth**, and reuse-previous starts mutually exclusive. Truth uses the - complex field stored by ModelPNPS (and reports an explicit error for an older - intensity-only file). **Reuse previous result** warm-starts one run from the - last — handy for the global-then-local refine recipe. It seeds the previous **spectrum** as the - initial guess *and* the extras that run fitted (thickness, τ₀, smearing width) - as the starting values for fitting them again, naming them in the status line; - an extra you are no longer fitting keeps the value set in the controls. When - the run finishes, the status line reports the fitted extras alongside the - error and FWHMs — including the geometric-smearing width as a delay σ in fs - with its multiplier on the nominal mask geometry (which is left untouched). When the trace came from the synthetic or - simulated workflow, the **known-truth** temporal intensity is drawn as a thin - black line beneath the retrieved and transform-limited pulses. When its complex - field is available, dotted truth-phase lines appear in both time and wavelength - panels and are named in their legends. The truth arrays are also written into - the saved `result.h5`. - - Beneath the pages sits a **numeric read-out** that updates as the run - converges; click its header to fold it away and give the plots its height - (remembered between launches). Its *Pulse parameters* table has one row per - curve in the figure, - labelled to match the legends — `R` (retrieved), `M` (the independent measured - spectrum), `TL` (transform-limited) and `truth` — and four columns: peak - wavelength, spectral FWHM, temporal FWHM and peak power. Cells that are not - defined show an en dash rather than a misleading number: the measured spectrum - carries no phase, so it has no temporal envelope; the transform-limited pulse is - the retrieved $|E(\omega)|$ with the phase removed, so its spectrum *is* the - retrieved one and is reported once, on the `R` row. The peak-power column needs - a measured **pulse energy** (set it on the Load or Dispersion stage) and shares - one SI prefix, shown in its header, so the values compare at a glance. - - Below the table, `Iteration`, `Elapsed` and `Error R` track the run itself, and - `τ₀`, `Thickness` and `Smearing` show the forward model's extra parameters, each - marked **(fit)** or **(fixed)** so it is clear whether the retriever was free to - move it. The three run counters tick every iteration; the table refreshes with - the live full-plot preview, so with that preview turned off it stays blank until - the run finishes. - - The last line, `ε(Iₜ)`, `ε(I𝜔)` and `ε(E𝜔)`, appears when the trace came from - the synthetic or simulated workflow and so has a **known** pulse: the temporal - intensity, spectral intensity and complex-field errors against it - ({mod}`croak.truth_metrics`). They answer a different question from the - `Error R` above them — that scores the fit to the *trace*, these score the - distance to the true *pulse*, and a FROG trace does not determine the pulse - uniquely, so a small `R` can sit on a wrong answer. Read them together: a small - `ε(I𝜔)` with a large `ε(E𝜔)` means the amplitude is right and the phase is not. - The two spectral ones need the truth's complex field, so an intensity-only file - shows `ε(Iₜ)` alone rather than a number derived from something else. - - Forward-model groups sit alongside the solver options. *Dispersive - propagation* models a slab (material, thickness, Δz/Npoints — see - [The forward model](../explanation/forward_model.md)). *Geometrical smearing* - models the tilted-pulse-front temporal blur of a non-collinear BOXCARS setup - from the mask hole diameter and edge-to-edge spacing, with the resulting delay - width shown — and editable, which back-computes the spacing. Both groups grey - out for solvers that cannot use them; smearing needs one of the autodiff - solvers. The chromatic focal and collection settings are described in - [collection aperture modelling](collection_aperture.md). See also - [geometric smearing](geometric_smearing.md) for the reduced model. -8. **Dispersion** — apply and auto-tune residual dispersion - ({mod}`croak.dispersion`) to compress the retrieved pulse, including real glass - (built-in or [custom](../howto/materials_and_mirrors.md) materials) and a **mirror stack** — add rows of `(mirror, polarisation, bounces, - remove/add)` to back-propagate (remove) beam-path coating mirrors or add - chirped-mirror bounces — with the temporal profile and spectrogram updating as you - drag the sliders. Enter a **pulse energy** (J) (shared with the Load stage) to - switch the temporal plot to absolute power and read off the current and - transform-limited **peak power**; compressing the pulse raises the peak power - towards the transform-limited maximum at fixed energy. -9. **Uncertainty** — put an error bar on the retrieved FWHM - ({mod}`croak.uncertainty`). Choose the estimator (parametric or resampling - bootstrap, the substrate-thickness systematic, or the fast analytic - covariance), set the replicate count and confidence level, and run it on a - worker thread with a progress bar. The stage reports the point estimate with - its 68 % and 95 % intervals, draws the temporal confidence band over the - retrieved profile, and saves everything alongside the result. See - [Estimating FWHM uncertainty](uncertainty.md). +### Data + +Data has two steps, shown as segments at the top of the page: the loader for the +session's kind of data, then **Check the marginal**. The check step opens once a +trace is loaded. **Continue** on the loader moves to the check, and Continue on +the check moves to Prepare. + +#### Load the trace (measured) + +Pick an HDF5/NPZ file and the trace/wavelength/delay datasets, with unit and axis +auto-detection ({mod}`croak.io`), then press **Load & Preview**; Continue is +available once the trace has loaded. An optional independent spectrum can come +from HDF5, NPZ or CSV, with background subtraction, and wavelength calibration +curves from CSV. **Transpose** fixes a trace that loaded as (delay × +wavelength), and **Reverse delay axis** flips the delay-sign convention — +negating the scan axis, so the trace is mirrored about τ = 0. Which end of a +stage scan is "gate late" is a wiring convention the file does not record. It +matters for **PG and SD**, whose kernels fix the direction of time: a wrong +setting time-reverses the retrieved pulse and flips the sign of every phase +order. The error is invisible on a symmetric transform-limited pulse, so check +the setting once against a pulse of known chirp (for example after adding a +known glass thickness). An **SHG** trace is symmetric in delay even for a +chirped pulse, so reversing it changes nothing and cannot settle SHG's own +[direction-of-time ambiguity](../explanation/pnps_framework.md#ambiguities) — +that is resolved after retrieval instead +([SHG: settle the direction of time first](postprocessing.md#shg-settle-the-direction-of-time-first)). +It is the measured counterpart of the simulated loader's switch of the same +name. + +Optionally enter the measured **pulse energy** (J) here to rescale the temporal +plots to absolute power (see the Dispersion page under [Analyse](#analyse)). +The trace preview uses the same colour scheme as Prepare (white→viridis for +positive, white→red for negative), with the display scaled to the in-trust-band +signal so an aggressive efficiency tilt cannot wash the image out. + +#### Load the scan (simulated) + +Load a numerically simulated `scansave` scan and preview it live. A +**thickness** dropdown selects which saved propagation depth to retrieve from a +multi-thickness scan (disabled for older single-thickness files). Continue loads +the scan and moves to the marginal check; with **Skip filtering & regrid (raw → +retrieval)** ticked it builds the as-simulated trace on its native grid and goes +straight to Retrieve. See +[Loading numerically simulated traces](loading_simulated.md). + +#### Generate the trace (synthetic) + +Build a test trace (pulse shape, interaction, delays, optional dispersive slab +and noise) using {func}`~croak.forward.maketrace`, previewed live. Useful for +trying the workflow without data. The **duration** is the intensity (power) FWHM +of the temporal envelope — both Gaussian and sech² honour this convention +({func}`~croak.pulses.gaussian_pulse`, {func}`~croak.pulses.sech_pulse`). The +delay and wavelength samplings each expose an interlocked **count** and +**step** at fixed range (editing one updates the other), and a **Spectrum +(independent)** section generates a fundamental spectrum that is passed +downstream (range / step / # points); by default it **mirrors the FROG-trace λ +window**, scaled to the fundamental (×2 for SHG). With it present, the +marginal-check overlay and the SHG marginal correction work on synthetic traces +too. Continue generates the trace and moves to the marginal check. The known +pulse (its temporal intensity envelope `It`) is carried through, shown beneath +the retrieved pulse, and saved with the result. + +#### Check the marginal + +Compare the trace's frequency marginal with the marginal **predicted** from the +independent spectrum ({mod}`croak.marginal_checks`): exact for SHG (the spectral +autoconvolution) and transform-limited for PG/TG/SD, within the thin local +model; the [marginal guide](marginal_checks.md) states its assumptions. The page +overlays the predicted marginal, marks the predicted vs measured **centroid**, +reports the **width-bracket** check, and exposes the spectral **trust region** +(defaults to 190–1000 nm for a measured trace, or the full loaded range for the +noise-free simulated/synthetic traces), a **frequency/wavelength** axis toggle +(default frequency, the space the retrieval works in; the wavelength view +applies the $1/\lambda^2$ density Jacobian, which shifts a broad marginal towards +the blue), a **log/linear** marginal toggle (default linear, peak-normalised) +and the **3rd-order** `(λ/µm)^exp` efficiency correction. Its **Auto** button +picks the exponent whose corrected-marginal centroid matches the prediction +({func}`~croak.marginal_checks.auto_third_order_exponent`). The **SHG marginal +correction** is available here: it scales the trace's frequency rows onto the +predicted SHG autoconvolution +({func}`~croak.marginal_checks.apply_marginal_correction`), is enabled only for +an SHG trace with an independent spectrum, and previews the snap onto the +prediction live (the correction itself is applied in Prepare). All three kinds +of session pass through this step, except a raw simulated scan; see +[Marginal consistency checks](marginal_checks.md). The trace and the marginal +comparison share a draggable divider, so either plot can be given the height. + +### Prepare + +Fringe/DC/low-pass filtering, windowing and regridding +({func}`~croak.preprocess.load_and_clean`), recomputed automatically as you +change a setting. The plots show six maps in two rows of three — the measured, +filtered and regridded trace, in dB above and linear below — with linked axes: +all six share the delay axis, and the measured and filtered maps share the +wavelength axis, so a zoom on one follows on the others. + +The controls always show the settings that were actually applied: re-entering +the stage re-seeds every widget from the current parameters, and those +parameters survive a round trip back to the loader as long as the trace is +unchanged (see [Preprocessing](preprocessing.md#settings-across-a-re-load)). +**Reset defaults** restores the data-seeded defaults. With **Baseline (arPLS)** +on, the **Auto** button beside *τ exclude (fs)* sets the hold-out to twice the +loaded trace's delay-marginal FWHM. Use it on measured traces: without a +hold-out the baseline bows into the τ ≈ 0 peak and removes signal, worst on the +rows that constrain the fit least. The failure shows up as a thin stripe at one +wavelength in the cleaned trace (see +[Peak suck-up](preprocessing.md#peak-suck-up-and-the-hold-out)). + +### Retrieve + +Choose the algorithm (any of the ten, including the global +[`cma-es`](../explanation/algorithms.md#global-retrieval-cma-es)) and its +parameters and run it with **Retrieve** in the top bar or **Run** on the form. +Retrieval runs in a background thread, driven by the solver `callback`, so the +interface stays responsive. The plots show a live overview: the measured and +retrieved traces (log) above the retrieved pulse and its spectrum. The full pages +of panels are on [Analyse](#analyse). + +The form's sections, in order, are the solver and its mode, the forward-model +sections (*Dispersive propagation*, *Geometrical smearing*), the advanced +sections, *Post-filter (applied to result)* and *Display*. The advanced sections — +*Chromatic focal mixture (advanced)*, *Extra fit parameters*, *Regularisation* +and *Global search (cma-es)* — start folded, and whether each is open is +remembered between launches. Below them are **Run**, **Stop**, **Save…** (which +writes `result.h5`, `options.toml` and `retrieval.pdf` into a folder), **Reset** +(the default retrieval settings) and **Retarget to new dataset…** (see +[Sessions](#sessions)). + +The **Display** section's **Spectrum axis** draws the spectrum panel against +**frequency** (PHz, the default — the space the retrieval works in) or +**wavelength** (nm, the spectrometer's axis, carrying the $1/\lambda^2$ density +Jacobian); both are unit-peak densities in their own variable, the choice is +remembered, and exported figures follow it. + +By default **Live full-plot preview** redraws the overview as the run converges. +Previews arrive at most four times a second, and less often on a machine where +presenting one is slow: the GUI measures what each preview costs and stretches +the interval so that presenting stays below about a quarter of the wall-clock +time, so the preview never starves the solver. Until the first preview arrives, +the convergence curve is shown instead, redrawn at up to ten frames a second +however fast the solver iterates. Later snapshots update the existing curves and +images in place, so nothing shifts between frames. Pressing **Stop** halts the +run at that point and leaves the latest result on screen as a (non-converged) +result you can inspect, save or post-filter. Untick **Live full-plot preview** to +fall back to the cheaper trace-error-vs-iteration curve (the full overview is +still drawn once the run finishes). Every solver supports the preview, including +`copra`/`copra-jax` and the COPRA warm-up of `warm-lbfgs`, which is 300 +iterations by default. `warm-lbfgs` runs **both** stages on one convergence +curve; the convergence panel on Analyse's Diagnostics page marks the handover +with a dashed rule. Read the two sides as different units of work (COPRA +iterations, then L-BFGS function evaluations). + +The **Initial guess** selector makes automatic, transform-limited, random, +perfect-Gaussian, known **truth**, and reuse-previous starts mutually exclusive. +Truth uses the complex field stored by ModelPNPS (and reports an explicit error +for an older intensity-only file). **Reuse previous result** warm-starts one run +from the last — handy for the global-then-local refine recipe. It seeds the +previous **spectrum** as the initial guess *and* the extras that run fitted +(thickness, τ₀, smearing width) as the starting values for fitting them again, +naming them on the ready line; an extra you are no longer fitting keeps the value +set in the controls. When the run finishes, the ready line reports the fitted +extras alongside the error and FWHMs — including the geometric-smearing width as +a delay σ in fs with its multiplier on the nominal mask geometry (which is left +untouched). When the trace came from a synthetic or simulated session, the +**known-truth** temporal intensity is drawn as a thin black line beneath the +retrieved and transform-limited pulses. When its complex field is available, +dotted truth-phase lines appear in both time and spectrum panels and are named +in their legends. The truth arrays are also written into the saved `result.h5`. + +Beneath the plots sits a **numeric read-out** that updates as the run +converges; click its header to fold it away and give the plots its height +(remembered between launches). Its *Pulse parameters* table has one row per +curve in the figure, labelled to match the legends — `R` (retrieved), `M` (the +independent measured spectrum), `TL` (transform-limited) and `truth` — and four +columns: peak wavelength, spectral FWHM, temporal FWHM and peak power. Cells +that are not defined show an en dash rather than a misleading number: the +measured spectrum carries no phase, so it has no temporal envelope; the +transform-limited pulse is the retrieved $|E(\omega)|$ with the phase removed, +so its spectrum *is* the retrieved one and is reported once, on the `R` row. The +peak-power column needs a measured **pulse energy** (set it on the measured +loader or on the Dispersion page) and shares one SI prefix, shown in its header, +so the values compare at a glance. + +Below the table, `Iteration`, `Elapsed` and `Error R` track the run itself, and +`τ₀`, `Thickness` and `Smearing` show the forward model's extra parameters, each +marked **(fit)** or **(fixed)** so it is clear whether the retriever was free to +move it. The three run counters tick every iteration; the table refreshes with +the live full-plot preview, so with that preview turned off it stays blank until +the run finishes. + +The last line, `ε(Iₜ)`, `ε(I𝜔)` and `ε(E𝜔)`, appears when the trace came from a +synthetic or simulated session and so has a **known** pulse: the temporal +intensity, spectral intensity and complex-field errors against it +({mod}`croak.truth_metrics`). They answer a different question from the +`Error R` above them — that scores the fit to the *trace*, these score the +distance to the true *pulse*, and a FROG trace does not determine the pulse +uniquely, so a small `R` can sit on a wrong answer. Read them together: a small +`ε(I𝜔)` with a large `ε(E𝜔)` means the amplitude is right and the phase is not. +The two spectral ones need the truth's complex field, so an intensity-only file +shows `ε(Iₜ)` alone rather than a number derived from something else. + +*Dispersive propagation* models a slab (material, thickness, Δz/Npoints — see +[The forward model](../explanation/forward_model.md)). *Geometrical smearing* +models the tilted-pulse-front temporal blur of a non-collinear BOXCARS setup +from the mask hole diameter and edge-to-edge spacing, with the resulting delay +width shown — and editable, which back-computes the spacing. Both sections grey +out for solvers that cannot use them; smearing needs one of the autodiff +solvers. The chromatic focal and collection settings are described in +[collection aperture modelling](collection_aperture.md). See also +[geometric smearing](geometric_smearing.md) for the reduced model. + +### Analyse + +Analyse reads the retrieved result. A rail on the left lists its pages; click +one, or step through them with the Up and Down arrow keys: + +- **Overview** — the live overview from Retrieve, without the form beside it; +- **Traces** — the measured and retrieved trace, log and linear; +- **Pulse** — the retrieved pulse over the delay marginal, and the spectrum + over the frequency marginal; +- **Diagnostics** — convergence, residuals, spectral filter and spectrogram; +- **Dispersion** and **Uncertainty**, described below. + +Overview, Traces, Pulse and Diagnostics are the panels of +{func}`~croak.plotting.plot_retrieval`, two by two so that each has room even on +a laptop display. A page is redrawn when it is shown, so the spectrogram, the +costliest panel, is computed only when you open Diagnostics. Each has +**Export ▾** with **Figure…** and **Pop out** (see [Plots](#plots)). The primary +action on these pages, and on Dispersion, is **Save**: it saves the session. + +#### Dispersion + +Apply and auto-tune residual dispersion ({mod}`croak.dispersion`) to compress +the retrieved pulse, including real glass (built-in or +[custom](../howto/materials_and_mirrors.md) materials), gases and a **mirror +stack** — add rows of `(mirror, polarisation, bounces, remove/add)` to +back-propagate (remove) beam-path coating mirrors or add chirped-mirror bounces — +with the temporal profile and spectrogram updating as you drag the sliders. +Enter a **pulse energy** (J) (shared with the measured loader) to switch the +temporal plot to absolute power and read off the current and transform-limited +**peak power**; compressing the pulse raises the peak power towards the +transform-limited maximum at fixed energy. The page's own **Save…** writes the +dispersed result as `result_dispersion.h5`, with `options.toml` and +`dispersion.pdf`. + +#### Uncertainty + +Put an error bar on the retrieved FWHM ({mod}`croak.uncertainty`). Choose the +estimator (parametric or resampling bootstrap, the substrate-thickness +systematic, or the fast analytic covariance), set the replicate count and +confidence level, and run it with **Estimate** (on the page or in the top bar) +on a worker thread; the ready line counts the replicates, and **Stop** cancels +the run. The page reports the +point estimate with its 68 % and 95 % intervals, draws the temporal confidence +band over the retrieved profile, and its **Save…** appends the estimates to +`result.h5` and writes `options.toml` and `uncertainty.pdf`. See +[Estimating FWHM uncertainty](uncertainty.md). ## Sessions -The GUI persists its settings — preprocessing choices, algorithm and options — to -a TOML file via {func}`croak.save.save_options` / {func}`croak.save.load_options`, -so you can reload a prior session. Retrievals are saved to HDF5 with -{func}`croak.save.save_result`. **Load previous retrieval** reloads the saved -`options.toml`; if a `result.h5` sits beside it, the retrieved result is -**restored from the file** ({func}`croak.save.result_from_saved`) after the cheap -load + preprocess steps replay — so the solver is **not** re-run (it falls back to -re-running only if the file is missing or its grid no longer matches). See -[Saving and loading](saving_and_loading.md). +A session is the settings of every stage — the loader, preparation, retrieval, +dispersion and uncertainty choices — kept as a TOML file +({func}`croak.save.save_options` / {func}`croak.save.load_options`), with the +retrieval in HDF5 ({func}`croak.save.save_result`). + +**Save session…** (⋯ menu, File menu, or the primary action on Analyse) asks for +a folder and writes three files into it: `result.h5` (the retrieval, with any +uncertainty estimates appended), `options.toml` (the settings) and +`retrieval.pdf` (the twelve-panel overview, drawn by matplotlib). The session is +added to Recent sessions and its folder's name appears in the top bar. Saving +needs a retrieval: before one, the GUI says so. + +**Open a session…** (Home, ⋯ menu or File menu) or a click on a recent session +reopens a saved `options.toml` and replays it: + +1. The trace is reloaded with the saved loader settings and prepared with the + saved preparation settings. +2. If a `result.h5` sits beside the `options.toml`, its retrieval is + **restored from the file** ({func}`croak.save.result_from_saved`) without + re-running the solver — but only when it was fitted to exactly the trace the + replay produced. The saved measured trace and frequency axis are compared + element by element ({func}`croak.session.files.result_mismatch`). +3. Otherwise the retrieval is re-run, and the ready line on Retrieve says why + (no saved result, an unreadable file, or a trace that differs). + +A synthetic session cannot be replayed this way: the generator's settings are not +part of the saved options, so the GUI loads the settings and asks you to +regenerate the trace. If the trace cannot be reloaded (a file has moved, say), +the loader says why. See [Saving and loading](saving_and_loading.md). + +**Retarget to new dataset…** on the Retrieve form, available once a retrieval +has run, points a measured session at another dataset folder with the same file names, keeps the calibration files, and +replays load → prepare → retrieve with the current settings +({func}`croak.session.retarget.retarget`). Only a measured session has such a +folder; for the others the status bar gives the reason instead. + +**Show the settings (options.toml)** and **Show as a Python script** open a +drawer on the right of the window with the session's settings as the +`options.toml` it would save, or as a standalone script that replays it without +the GUI ({func}`croak.scripting.generate_script`). **Copy** puts the text on the +clipboard; **Close** hides the drawer. ## Scripting the same workflow -Everything in the wizard maps to a few library calls — load → clean → retrieve → +A saved session runs without the GUI: `croak replay options.toml` replays it +headlessly, and `croak script options.toml` writes the same Python script that +**Show as a Python script** shows (see +[saving, sessions, and reproducible workflows](../user_guide/workflows.md)). + +Everything in the GUI maps to a few library calls — load → clean → retrieve → process → plot → save: ```python diff --git a/docs/howto/loading_simulated.md b/docs/howto/loading_simulated.md index 2710bfa..33cc8b1 100644 --- a/docs/howto/loading_simulated.md +++ b/docs/howto/loading_simulated.md @@ -196,7 +196,7 @@ retrieved spectrum blue-shifted. Three alternatives avoid the guesswork: If you trust the measured gating spectrum, **Rω with a light `reg_spectrum`**, or **phase-only** retrieval, removes the exponent; a single exponent (the -marginal-check stage's **Auto** picks it from your spectrum) is the quick +marginal check's **Auto** picks it from your spectrum) is the quick approximation. None of these corrections removes delay-dependent collection effects, which need the [collection model](collection_aperture.md). @@ -314,8 +314,8 @@ The options live on {class}`~croak.session.params.SimulatedLoadParams`: ```{note} **What "load λ band" does.** `lam_min_nm`/`lam_max_nm` crop the *loaded* trace to bins with λ in that range (and always drop the unphysical ω ≤ 0 bins). It sets the -**extent of the data** handed to the marginal-check and preprocess stages — it is -*not* the retrieval grid (that is the Preprocess stage's own λ range). The GUI +**extent of the data** handed to the marginal check and Prepare — it is +*not* the retrieval grid (that is Prepare's own λ range). The GUI defaults it to the file's **full** positive-frequency span; narrow it only to crop noisy or empty wings. ``` @@ -324,7 +324,7 @@ noisy or empty wings. TG-FROG keeps the carrier (`omega0_scale = 1`), so the trace and the pulse sit at the same wavelengths and `"pg"`/`"sd"` are the only sensible interactions — never `"shg"`. The default `third_order_exp = 2.0` is the radiated-field $\omega^2$ -factor; the marginal-check stage's **Auto** refines it against your spectrum (the +factor; the marginal check's **Auto** refines it against your spectrum (the `Iω_win` + beamlet combination usually wants a little more, the `"source"` spectrum more again). It is only an approximation — see the correction options above (Rω / phase-only / calibration). @@ -361,7 +361,7 @@ data = assemble_simulated_load_data( ) ) -# stage 2 onward is identical to a measurement: filter / window / regrid … +# from here on it is identical to a measurement: filter / window / regrid … td = build_tracedata( PreprocParams(lam_min_nm=200, lam_max_nm=340, lamm_min_nm=210, lamm_max_nm=330), data, @@ -426,21 +426,23 @@ delay resampling is needed. ## From the GUI -The welcome menu's **Load simulated trace** entry opens a loader page: choose the -`.h5` file, the trace window, the **thickness**, the corrections, the -**reference spectrum** and the **time-domain truth** source, then preview the trace -live and press the footer's **Load & continue →** button (bottom right, like every -other page) to hand it straight to the **Preprocess** stage. The +The **Simulated scan** tile on Home starts a session on the loader, the first +step of the Data stage: choose the `.h5` file, the trace window, the +**thickness**, the corrections, the **reference spectrum** and the **time-domain +truth** source, then preview the trace live and press **Continue** (the primary +action, at the right of the top bar) to load it and move to the marginal check, +and from there to **Prepare**. The **time-domain truth** dropdown lists what the file stores (the beamlet first, so `It_beamlet` is the default) and is disabled when the file offers only one. For a multi-thickness file the **thickness** dropdown lists every saved depth (largest/exit first, the default); changing it re-previews the trace at that depth. For an older single-thickness file the dropdown -is disabled (only the exit slice exists). Every control has a hover tooltip, and the **? Help** button -summarises the options (including the exponent/reference guidance above). From the -filter stage on, the workflow is exactly as for a measured trace — so you can apply -croak's fringe/DC/threshold filters to the modelled data too. **Back** from -preprocess returns to the loader page to adjust the options. +is disabled (only the exit slice exists). Every control has a hover tooltip, and +**Help for this stage** (⋯ menu or F1) summarises the options (including the +exponent/reference guidance above). From Prepare on, the workflow is exactly as +for a measured trace — so you can apply croak's fringe/DC/threshold filters to the +modelled data too. The **Data** button in the top bar returns to the loader to +adjust the options. The preview is shown in the **file-native representation**: the simulated trace and its spectral marginal are an **angular-frequency density** plotted against @@ -450,12 +452,12 @@ stays in fs. The **load λ band** seeds to the file's full range (see the note above). Tick **Skip filtering & regrid (raw → retrieval)** to load the as-simulated trace -on its native grid **straight into the Retrieve stage**, bypassing the -marginal-check and preprocess stages (see -[above](#raw-direct-to-retrieval)); the footer's forward button relabels to -**Load raw → Retrieve →** to say so. The known-truth overlay is still shown, and -**Back** from Retrieve reaches Preprocess so you can run the regrid path and -compare. +on its native grid **straight into the Retrieve stage** when you press Continue, +bypassing the marginal check and Prepare (see +[above](#raw-direct-to-retrieval)); a scan loaded this way leaves Prepare +disabled, and its tooltip says why. The known-truth overlay is still shown. To +compare with the regrid path, untick the option on the loader and Continue +again. The Rω / phase-only alternatives to the third-order exponent live on the **Retrieve** stage (the *Rω adaptive scaling* and *phase-only* toggles); turn the @@ -463,18 +465,19 @@ loader's third-order correction off and let Rω absorb the response there instea ## Saving and reloading a simulated session -Saving from the Retrieve stage writes the loader's options into the session file +Saving the session writes the loader's options into the session file as a `[simulated]` table, alongside `entry = "simulated"` (see [Saving and loading](saving_and_loading.md)) — the scan file, trace window, thickness slice, loaded band, third-order exponent, delay-sign convention and the spectrum/truth choices. So the retrieval records *which* simulated trace it came from and how that trace was built. -**Load previous retrieval** reads `entry` and reloads through this loader rather +**Open a session…** reads `entry` and reloads through this loader rather than the measured one: it re-reads the scan with the saved options (including the raw *skip filtering & regrid* path, which rebuilds the native-grid trace directly), restores the loader page's controls, replays the saved preprocessing -and rehydrates the sibling `result.h5` without re-running the solver. The same +and rehydrates the sibling `result.h5` without re-running the solver, when it +was fitted to the trace the replay produced. The same file replays headlessly: ```bash @@ -486,6 +489,6 @@ uv run croak replay options.toml another *per-dataset folder*, a convention of measured acquisitions (trace, spectrum and backgrounds sharing a folder with stable filenames). A simulated session is one scansave file with no such siblings, so retargeting it raises — and -the Retrieve stage's **Retarget…** button reports that instead of silently +the Retrieve stage's **Retarget to new dataset…** button reports that instead of silently replaying the same scan. ``` diff --git a/docs/howto/marginal_checks.md b/docs/howto/marginal_checks.md index 8a428b2..3cf4e95 100644 --- a/docs/howto/marginal_checks.md +++ b/docs/howto/marginal_checks.md @@ -97,9 +97,9 @@ collection effects. ## In the GUI -All of the above is interactive on the wizard's **Marginal check** stage, which -sits between the data loaders and preprocessing for the experimental, simulated -and synthetic entry paths. It overlays the predicted marginal on the measured +All of the above is interactive on the GUI's **Check the marginal** step, the +second step of the Data stage, which sits between the data loaders and Prepare +for measured, simulated and synthetic sessions. It overlays the predicted marginal on the measured one, marks the predicted vs measured centroid, reports the width-bracket check, and exposes the trust region, a log/linear toggle and the `(λ/µm)^exp` correction with an `Auto` (centroid-matching) button. The **SHG marginal correction** also diff --git a/docs/howto/postprocessing.md b/docs/howto/postprocessing.md index f64918a..ed720fa 100644 --- a/docs/howto/postprocessing.md +++ b/docs/howto/postprocessing.md @@ -198,7 +198,7 @@ direction of time. Reversal is applied by conjugating the spectrum, which is exact: the stored trace, `mu` and `error` remain valid. Note that the **spectral phase changes sign**, so the retrieved GDD and TOD only become comparable with the truth once the direction -is settled. The wizard does this automatically for synthetic and simulated +is settled. The GUI does this automatically for synthetic and simulated sessions, where a truth is available. ### Framing the axes @@ -246,8 +246,8 @@ croak.plotting.draw_spectral(ax, data) # one panel, in your own figure `retrieved_log`, `measured_lin`, `retrieved_lin`, `temporal`, `spectral`, `convergence`, `residual`, `spectral_filter`, `freq_marginal`, `delay_marginal`, `spectrogram` — to their titles and draw functions; -{data}`~croak.plotting.RETRIEVAL_PAGES` holds the three 2×2 pages the wizard shows -as tabs (**Traces**, **Pulse**, **Diagnostics**), and +{data}`~croak.plotting.RETRIEVAL_PAGES` holds the three 2×2 pages the GUI shows +on Analyse (**Traces**, **Pulse**, **Diagnostics**), and {data}`~croak.plotting.RETRIEVAL_OVERVIEW` the 3×4 layout `plot_retrieval` draws. The spectrum panel is drawn against frequency by default; pass `spectral_axis="wavelength"` to `plot_retrieval` or `retrieval_plot_data` (or @@ -256,8 +256,8 @@ unit-peak densities in their own variable, so the wavelength view carries the $1 Jacobian and the frequency view does not. Colourbars shared by a pair of trace panels are drawn whenever both panels are on the page. Every `draw_*` returns the artists it created (for example -{class}`~croak.plotting.TemporalArtists`), which is what a live view updates in -place instead of redrawing. +{class}`~croak.plotting.TemporalArtists`), so you can restyle or annotate them +afterwards. Other composites and single-axis plotters: @@ -277,10 +277,11 @@ Other composites and single-axis plotters: The single-axis plotters take an existing Matplotlib axis, so you can compose your own figures. -### Live views (GUI support) +### Screen views (GUI support) -For custom interactive applications, see the [live-view contracts in the -developer’s guide](../developer_guide/gui_sessions.md#live-views-gui-support). +The GUI does not draw these matplotlib panels on screen: it draws the same data +with interactive views, from the Qt-free {mod}`croak.plotting.screen`. See +[screen views in the developer’s guide](../developer_guide/gui_sessions.md#screen-views-gui-support). ## Windowing the retrieved field diff --git a/docs/howto/preprocessing.md b/docs/howto/preprocessing.md index 4bebcb5..f000af4 100644 --- a/docs/howto/preprocessing.md +++ b/docs/howto/preprocessing.md @@ -291,7 +291,7 @@ fig = croak.plot_frog_filter(td) ## Settings across a re-load -In the [wizard](gui.md) the preprocess controls are bound one way: a widget +In the [GUI](gui.md) the Prepare controls are bound one way: a widget writes its parameter and triggers a recompute, so what the parameters say is what gets computed. To keep the two in step, **entering the stage re-seeds every widget from the current parameters**. That matters because loading a trace @@ -303,7 +303,7 @@ dataset or window selection, units, loaded band, orientation and interaction, that is, everything that changes the data-seeded defaults. * **Same trace** (reloading after changing a background, a calibration or the - third-order correction, or walking Back and forward again): your windowing + third-order correction, or going back to Data and forward again): your windowing and filter choices are kept; only the slider bounds are refreshed and the values clamped into them. * **Different trace**: the data-seeded defaults are restored, and since the @@ -341,7 +341,7 @@ against a known chirp. An SHG trace is symmetric in delay for any pulse settles nothing — SHG's direction-of-time ambiguity is resolved after retrieval with {func}`~croak.processing.resolve_time_direction`, see [Post-processing](postprocessing.md#shg-settle-the-direction-of-time-first). -The GUI's Load stage exposes both as the **Transpose** +The GUI's measured loader (on Data) exposes both as the **Transpose** and **Reverse delay axis** switches ({class}`~croak.session.params.LoadParams`); `croak.session` applies the flip in {func}`~croak.session.pipeline.assemble_load_data`, before the axis is sorted, so diff --git a/docs/howto/saving_and_loading.md b/docs/howto/saving_and_loading.md index 8c382d2..744fcfa 100644 --- a/docs/howto/saving_and_loading.md +++ b/docs/howto/saving_and_loading.md @@ -78,10 +78,11 @@ from croak.save import load_result, result_from_saved result = result_from_saved(load_result("retrieval.h5")) ``` -The GUI's **Load previous retrieval** uses exactly this: it replays the cheap +The GUI's **Open a session…** uses exactly this: it replays the cheap load + preprocess steps and then rehydrates the saved `result.h5` onto the matching -grid, only re-running the solver if the file is absent or the grid no longer -matches. +grid, only re-running the solver if the file is absent, cannot be read, or was not +fitted to exactly the trace the replay produced +({func}`croak.session.files.result_mismatch`). ## Session options → TOML @@ -118,7 +119,8 @@ entry = "simulated" [uncertainty] ``` -`entry` names the **stage-1 loader the trace came from** — `"experimental"`, +`entry` names the **loader the trace came from** (the first step of the GUI's +Data stage) — `"experimental"`, `"simulated"` or `"synthetic"` — and so which of `[load]` / `[simulated]` is the live one; the other is written at its defaults. Without it a simulated session would be indistinguishable from an experimental one with an empty file path, and diff --git a/docs/howto/uncertainty.md b/docs/howto/uncertainty.md index 825a089..e7c8f7a 100644 --- a/docs/howto/uncertainty.md +++ b/docs/howto/uncertainty.md @@ -209,7 +209,7 @@ print(u.summary()) # FWHM = … ± … fs at −1 mm SiO2 (…) The same `propagation=H` works for {func}`~croak.covariance.covariance_uncertainty` and the other estimators. The intervening dispersion is assumed exactly known. In the GUI, tick -*"at dispersion-stage point"* to reuse the Dispersion stage's settings as $H$. +*"at dispersion-stage point"* to reuse the Dispersion page's settings as $H$. ## Reading and plotting the result @@ -282,10 +282,10 @@ retrievals — keep `n_trials`/`n_resamples` modest while exploring). ## In the GUI -The wizard's **Uncertainty** stage (after Dispersion) runs the bootstrap in the background +The GUI's **Uncertainty** page (under Analyse) runs the bootstrap in the background on the current retrieval, using the same solver settings, and plots the result. Pick the method, the replicate count and (for the parametric method) the noise source, then click -**Estimate**; **Stop** cancels mid-run. For a dispersive retrieval, choose method +**Estimate** (on the page or in the top bar); **Stop** cancels mid-run. For a dispersive retrieval, choose method **thickness** and set **Thickness σ (µm)** to your measured substrate-thickness uncertainty (the central value is the retrieval's own thickness) to get the systematic FWHM error bar. diff --git a/docs/introduction/interfaces.md b/docs/introduction/interfaces.md index ba41c6a..f910190 100644 --- a/docs/introduction/interfaces.md +++ b/docs/introduction/interfaces.md @@ -8,8 +8,9 @@ your own analysis. You can move between these approaches using saved sessions. ## Work in the GUI After [installing the GUI](../getting_started/gui.md), launch `croak-gui`. -The wizard guides you through loading data, preparing a trace, retrieving a -pulse, and analysing the result. The [GUI guide](../howto/gui.md) describes the +Its window takes you through loading data, preparing a trace, retrieving a +pulse, and analysing the result, and can show the session's settings as a Python +script. The [GUI guide](../howto/gui.md) describes the controls and stages; [preparing and checking data](../user_guide/data.md) explains the choices behind them. diff --git a/docs/reference/api/workflow.md b/docs/reference/api/workflow.md index e5859b6..4af64e7 100644 --- a/docs/reference/api/workflow.md +++ b/docs/reference/api/workflow.md @@ -110,9 +110,9 @@ different beamline point. The background and caveats are in A whole retrieval — load, preprocess, retrieve, dispersion compensation and uncertainty — captured as serialisable per-stage parameters and replayed without the GUI. {func}`~croak.session.engine.run_session` drives the same stage mappings the -wizard uses; {func}`~croak.session.retarget.retarget` re-points an *experimental* session at +GUI uses; {func}`~croak.session.retarget.retarget` re-points an *experimental* session at a new dataset folder. {class}`~croak.session.options.SessionOptions` also records -which stage-1 loader the trace came from (`entry`, see +which loader the trace came from (`entry`, see {data}`~croak.session.options.ENTRIES`), so a simulated session replays through {func}`~croak.session.pipeline.assemble_simulated_load_data` rather than the measured loader — see [Saving and loading](../../howto/saving_and_loading.md). diff --git a/docs/reference/features.md b/docs/reference/features.md index 29300cf..b7d7d42 100644 --- a/docs/reference/features.md +++ b/docs/reference/features.md @@ -234,18 +234,20 @@ Further reading: [Estimating FWHM uncertainty](../howto/uncertainty.md), ## Interfaces - **Library first** — every stage is callable without the GUI. -- **The wizard** — a six-stage PySide6 application (Load → Marginal check → - Preprocess → Retrieve → Dispersion → Uncertainty) with live retrieval previews and a numeric - read-out of retrieved, measured, transform-limited and (for synthetic - sessions) true pulse parameters. Laid out for 1366×768 displays and up. - Importing `croak` does not import Qt. +- **The GUI** — a PySide6 window that takes a session from Home through Data + (load and marginal check) → Prepare → Retrieve → Analyse (the retrieval's + panels, Dispersion and Uncertainty), with interactive pyqtgraph plots, live + retrieval previews and a numeric read-out of retrieved, measured, + transform-limited and (for synthetic sessions) true pulse parameters. It shows + a session's settings as `options.toml` or as a Python script on request. Laid + out for 1366×768 displays and up. Importing `croak` does not import Qt. - **Sessions** — the session engine replays a saved TOML session end to end through the same stage operations the GUI uses. - **CLI** — `croak replay` reruns a session headlessly, including batch retargeting across datasets; `croak script` generates a standalone, editable Python script from it. -Further reading: [The wizard GUI](../howto/gui.md), +Further reading: [Using the GUI](../howto/gui.md), [Saving and loading](../howto/saving_and_loading.md). ## Conventions diff --git a/docs/reference/naming.md b/docs/reference/naming.md index be62492..836ba27 100644 --- a/docs/reference/naming.md +++ b/docs/reference/naming.md @@ -35,7 +35,7 @@ through AD. ## The mascot -The mascot was not renamed. The wizard's icon is still a toad, which — given what +The mascot was not renamed. The GUI's icon is still a toad, which — given what toads are known for — remains approximately correct. ```{note} diff --git a/docs/tutorials/05_dispersion_tuning.md b/docs/tutorials/05_dispersion_tuning.md index 4d2f678..04da1e3 100644 --- a/docs/tutorials/05_dispersion_tuning.md +++ b/docs/tutorials/05_dispersion_tuning.md @@ -102,7 +102,8 @@ print(f"peak power = {pr.peak_power/1e6:.1f} MW") print(f"TL peak power = {pr.peak_power_tl/1e6:.1f} MW") # the maximum achievable ``` -In the GUI's Dispersion stage this is shown live as you tune the sliders. See +On the GUI's Dispersion page (under Analyse) this is shown live as you tune the +sliders. See [Post-processing](../howto/postprocessing.md#absolute-power-and-peak-power). ## Other knobs diff --git a/docs/user_guide/workflows.md b/docs/user_guide/workflows.md index 20da24e..dc3e293 100644 --- a/docs/user_guide/workflows.md +++ b/docs/user_guide/workflows.md @@ -3,14 +3,14 @@ A result is reproducible when both the retrieved field and the settings that produced it are kept. croak saves results to HDF5 and every setting of an analysis to a TOML session file, which can be replayed without the GUI or turned -into an editable Python script. This chapter covers the graphical wizard, which -writes those sessions, and the saving and loading functions behind it. +into an editable Python script. This chapter covers the graphical interface, +which writes those sessions, and the saving and loading functions behind it. ## Which page do I need? | I want to… | Read | |---|---| -| use the wizard stage by stage | [The wizard GUI](../howto/gui.md) | +| use the GUI stage by stage | [Using the GUI](../howto/gui.md) | | save a result, or several, and load them back | [Saving and loading](../howto/saving_and_loading.md) | | rerun a saved session headlessly or retarget it at new data | [Saving and loading](../howto/saving_and_loading.md) | | decide between the GUI and a script | [GUI or script interface](../introduction/interfaces.md) | From 021f5cea76d7e673f6ceca4cd6cc12ea895737cc Mon Sep 17 00:00:00 2001 From: John Travers Date: Fri, 6 Nov 2026 09:50:19 +0000 Subject: [PATCH 21/41] Draw the read-out fold's chevrons from SVG superqt painted its text arrows into a pixmap sized by the font's point size, -1 under the style sheet's pixel sizes, so macOS printed QPainter warnings at every session. The chevron is redrawn afresh for croak. Co-Authored-By: Claude Opus 5.5 --- croak/gui/assets/chevron-right.svg | 1 + croak/gui/assets/chevron.svg | 7 +------ croak/gui/controls.py | 11 ++++++++++- tests/gui/test_controls.py | 12 ++++++++++++ 4 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 croak/gui/assets/chevron-right.svg diff --git a/croak/gui/assets/chevron-right.svg b/croak/gui/assets/chevron-right.svg new file mode 100644 index 0000000..3c4d81a --- /dev/null +++ b/croak/gui/assets/chevron-right.svg @@ -0,0 +1 @@ + diff --git a/croak/gui/assets/chevron.svg b/croak/gui/assets/chevron.svg index c3a423c..8413b9f 100644 --- a/croak/gui/assets/chevron.svg +++ b/croak/gui/assets/chevron.svg @@ -1,6 +1 @@ - - + diff --git a/croak/gui/controls.py b/croak/gui/controls.py index 2e58b13..e83232d 100644 --- a/croak/gui/controls.py +++ b/croak/gui/controls.py @@ -7,6 +7,7 @@ from __future__ import annotations +from PySide6.QtGui import QIcon from PySide6.QtWidgets import ( QCheckBox, QComboBox, @@ -19,6 +20,7 @@ from . import settings from .sheet import Section +from .theme import ASSETS __all__ = [ "collapsible", @@ -61,7 +63,14 @@ def collapsible( ------- superqt.QCollapsible """ - strip = QCollapsible(title) + # Icons, not superqt's default "▼"/"▲" strings: it draws those into a + # pixmap sized by the font's *point* size, which is -1 for a pixel-sized + # font, and Qt then complains that it cannot paint on the empty pixmap. + strip = QCollapsible( + title, + expandedIcon=QIcon(str(ASSETS / "chevron.svg")), + collapsedIcon=QIcon(str(ASSETS / "chevron-right.svg")), + ) strip.setContent(content) strip.setDuration(120) # visible but brief if settings_key is not None: diff --git a/tests/gui/test_controls.py b/tests/gui/test_controls.py index 130a29e..b30dc04 100644 --- a/tests/gui/test_controls.py +++ b/tests/gui/test_controls.py @@ -28,3 +28,15 @@ def test_widget_factories_connect_the_change_handler(qapp): k = check("on", False, on_change=seen.append) k.setChecked(True) assert seen == [4, 0.5, "b", True] + + +def test_controls_collapsible_draws_its_chevrons_from_svg(qtbot): + from PySide6.QtWidgets import QLabel + + from croak.gui.controls import collapsible + + strip = collapsible("Read-out", QLabel("x")) + qtbot.addWidget(strip) + # Real icons, not superqt's text arrows painted into an empty pixmap. + for icon in (strip.expandedIcon(), strip.collapsedIcon()): + assert not icon.pixmap(12, 12).isNull() From f59d1c7aed78607185697e2e5bcc114a2e586e13 Mon Sep 17 00:00:00 2001 From: John Travers Date: Fri, 6 Nov 2026 09:50:19 +0000 Subject: [PATCH 22/41] =?UTF-8?q?Say=20on=20Prepare=20when=20the=20=CE=BB?= =?UTF-8?q?=20grid=20clips=20the=20trace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The regrid's taper-loss warning only reached the console. Prepare now shows it in the advice box with what to change, and marks failures as errors on the ready line. Co-Authored-By: Claude Opus 5.5 --- croak/gui/sheet.py | 6 ++++++ croak/gui/stage_preprocess.py | 32 ++++++++++++++++++++++++++---- tests/gui/test_sheet.py | 2 ++ tests/gui/test_stage_preprocess.py | 18 +++++++++++++++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/croak/gui/sheet.py b/croak/gui/sheet.py index 7f8958c..e1bf77f 100644 --- a/croak/gui/sheet.py +++ b/croak/gui/sheet.py @@ -353,6 +353,12 @@ def say(self, title: str, text: str) -> None: self.text.setText(text) self.show() + def clear(self) -> None: + """Hide the box: there is nothing to advise.""" + self.title.setText("") + self.text.setText("") + self.hide() + class DerivedGrid(QWidget): """Numbers derived from the settings: a small grey name over a large value.""" diff --git a/croak/gui/stage_preprocess.py b/croak/gui/stage_preprocess.py index ccb3048..95408f0 100644 --- a/croak/gui/stage_preprocess.py +++ b/croak/gui/stage_preprocess.py @@ -58,6 +58,11 @@ class _RangeKW(TypedDict): _RANGE_KW: _RangeKW = {"decimals": 1, "step": 0.1, "slider_scale": 10.0} +TAPER_ADVICE_LEVEL = 0.01 +"""The fraction of the trace the regrid's edge taper may remove before Prepare +says so: :func:`croak.preprocess.regrid`'s default ``taper_warn_level``, so the +page warns exactly when the library does.""" + class StagePreprocess(Stage): HELP_TITLE = "Preprocess" @@ -641,7 +646,7 @@ def apply(self, *, block: bool = False): try: td = pipeline.build_tracedata(params, data) # exact, full-resolution except Exception as exc: - self.set_status(f"Preprocess failed: {exc}") + self.set_status(f"Preprocess failed: {exc}", "error") return self._show_tracedata(td) return @@ -674,8 +679,27 @@ def _show_tracedata(self, td): self.state.set_tracedata(td) for key, view in filter_views(td).items(): self.views[key].show_view(view) - self.set_status( - f"Regridded to {td.grid.n}×{td.delays.size}. Proceed to Retrieve." + grid = f"Regridded to {td.grid.n}×{td.delays.size}." + if td.taper_loss > TAPER_ADVICE_LEVEL: + self.set_status(f"{grid} The λ grid clips the trace.", "warn") + self._advise_taper(td) + else: + self.set_status(f"{grid} Proceed to Retrieve.") + self.advice.clear() + + def _advise_taper(self, td): + """Say that the λ grid cuts live signal, and what to do about it. + + The library's own warning goes to the console, where a GUI user never + sees it; the clipped signal is discarded and the retrieved field is left + free at those frequencies, so the page says it where the user looks. + """ + self.advice.say( + f"The λ grid removes {td.taper_loss:.0%} of the trace", + f"The grid ({td.lam_min / 1e-9:.0f}–{td.lam_max / 1e-9:.0f} nm) ends " + "where the trace still has signal, so its edge taper discards that " + "signal and leaves the retrieved field unconstrained there. Widen the " + "λ range (nm) under Grid until the trace decays inside it.", ) def _on_preprocess_done(self, td, req_id, was_fast): @@ -696,4 +720,4 @@ def _on_preprocess_done(self, td, req_id, was_fast): def _on_preprocess_failed(self, message, req_id): if req_id != self._req_id: return - self.set_status(f"Preprocess failed: {message}") + self.set_status(f"Preprocess failed: {message}", "error") diff --git a/tests/gui/test_sheet.py b/tests/gui/test_sheet.py index 36c7cb9..9099cb5 100644 --- a/tests/gui/test_sheet.py +++ b/tests/gui/test_sheet.py @@ -183,6 +183,8 @@ def test_sheet_advice_box_appears_when_it_has_something_to_say(qtbot): box.say("Retrieval failed", "Try a smaller grid.") assert not box.isHidden() assert box.title.text() == "Retrieval failed" + box.clear() + assert box.isHidden() and box.title.text() == "" def test_sheet_derived_numbers_show_values_and_states(qtbot): diff --git a/tests/gui/test_stage_preprocess.py b/tests/gui/test_stage_preprocess.py index c439a59..3d435df 100644 --- a/tests/gui/test_stage_preprocess.py +++ b/tests/gui/test_stage_preprocess.py @@ -210,3 +210,21 @@ def test_stage_preprocess_baseline_toggle_gates_its_controls(stage): stage.baseline_check.setChecked(checked) assert stage.state.preproc.baseline is checked assert all(w.isEnabled() is checked for w in dependents) + + +def test_stage_preprocess_advises_when_the_grid_clips_the_trace(loaded): + from dataclasses import replace + + loaded.on_enter() + td = loaded.state.tracedata + assert loaded.advice.isHidden() # the seeded grid leaves room to spare + # Just over the library's own warning level: the page says so too. + clipped = replace(td, taper_loss=stage_preprocess.TAPER_ADVICE_LEVEL + 0.6) + loaded._show_tracedata(clipped) + assert loaded.status_label.state == "warn" + assert not loaded.advice.isHidden() + assert loaded.advice.title.text() == "The λ grid removes 61% of the trace" + assert "λ range (nm)" in loaded.advice.text.text() + loaded._show_tracedata(td) # widened again: the advice goes + assert loaded.advice.isHidden() + assert loaded.status_label.text().endswith("Proceed to Retrieve.") From c509d90518dcea48780ffc37a3097fddd68acc4e Mon Sep 17 00:00:00 2001 From: John Travers Date: Fri, 6 Nov 2026 09:52:48 +0000 Subject: [PATCH 23/41] Preview the synthetic trace as Continue hands it on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generator drew its trace on the whole simulation grid, where the signal band falls in one or two rows of the evenly resampled image and shows as a featureless stripe. It now draws the trace sampled onto the λ window, as the check shows it. A flat phase also no longer fills its axis with rounding noise: phase axes span at least ±1 rad. Co-Authored-By: Claude Opus 5.5 --- croak/gui/stage_synthetic.py | 33 ++++++++++++++++++++++--------- croak/plotting/pulse.py | 12 +++++++++-- croak/plotting/screen.py | 8 ++++++-- tests/gui/test_stage_synthetic.py | 20 +++++++++++++++++++ tests/test_plotting.py | 9 +++++++++ tests/test_plotting_screen.py | 9 +++++++++ 6 files changed, 78 insertions(+), 13 deletions(-) diff --git a/croak/gui/stage_synthetic.py b/croak/gui/stage_synthetic.py index e2b3252..5cc310e 100644 --- a/croak/gui/stage_synthetic.py +++ b/croak/gui/stage_synthetic.py @@ -444,10 +444,31 @@ def _recompute(self): self.set_status(f"Generation failed: {exc}") return self._last = (grid, ew, omega0) - # show the trace as it will be handed on (with any noise applied) - self._draw(grid, ew, omega0, lam_sig, delays, self._add_noise(trace)) + # Show the trace as Continue hands it on: sampled onto the λ window, with + # any noise. The raw trace spans the whole simulation grid, far wider + # than the signal, and drawn as it is it shows a featureless stripe. + lam, meas = self._measured(lam_sig, delays, trace) + if meas.max() <= 0: + self.set_status( + "Trace is empty in the chosen λ window; widen the range.", "warn" + ) + return + self._draw(grid, ew, omega0, lam, delays, meas / meas.max()) self.set_status("Adjust the pulse and the trace, then Continue to check it.") + def _measured(self, lam_sig, delays, trace): + """Sample the trace onto the user's λ window, as a spectrometer would. + + Returns ``(lam, trace)``: the window's wavelengths (m) and the trace on + them, with the chosen noise; zero outside the simulated band. + """ + lo, hi = self._wl[0] * 1e-9, self._wl[1] * 1e-9 + lam = np.linspace(lo, hi, int(self.nwl.value())) + meas = np.empty((lam.size, delays.size)) + for j in range(delays.size): + meas[:, j] = np.interp(lam, lam_sig, trace[:, j], left=0.0, right=0.0) + return lam, self._add_noise(meas) + def _draw(self, grid, ew, omega0, lam_sig, delays, trace): """Show the pulse, its spectrum and spectrogram, and its trace.""" temporal, spectral, gram = pulse_field_views(grid, ew, omega0) @@ -482,13 +503,7 @@ def advance(self): self.set_status(f"Generation failed: {exc}") return interaction = self.interaction.currentText() - # sample onto the user's measurement wavelength axis (emulate a spectrometer) - lo, hi = self._wl[0] * 1e-9, self._wl[1] * 1e-9 - lam_user = np.linspace(lo, hi, int(self.nwl.value())) - meas = np.empty((lam_user.size, delays.size)) - for j in range(delays.size): - meas[:, j] = np.interp(lam_user, lam_sig, trace[:, j], left=0.0, right=0.0) - meas = self._add_noise(meas) + lam_user, meas = self._measured(lam_sig, delays, trace) m = meas.max() if m <= 0: self.set_status("Trace is empty in the chosen λ window; widen the range.") diff --git a/croak/plotting/pulse.py b/croak/plotting/pulse.py index 0006c12..80252d9 100644 --- a/croak/plotting/pulse.py +++ b/croak/plotting/pulse.py @@ -95,10 +95,18 @@ def _intensity_ylim(*curves) -> tuple[float, float]: return 0.0, 1.05 * top if top > 0 else 1.0 +PHASE_MIN_HALF_SPAN = 1.0 +"""The least half-range of a phase axis (rad). + +A nearly flat phase would otherwise fill the axis with its rounding noise +(a transform-limited pulse's phase varies by ~1e-15 rad) and look structured.""" + + def _phase_ylim(*curves) -> tuple[float, float] | None: """Phase-axis limits centred on the shown phases with their span as margin. Finite samples of every non-empty curve count; ``None`` when nothing is shown. + The half-range is at least :data:`PHASE_MIN_HALF_SPAN`. """ arrays = [np.asarray(c, dtype=float) for c in curves if c is not None] arrays = [a[np.isfinite(a)] for a in arrays] @@ -106,9 +114,9 @@ def _phase_ylim(*curves) -> tuple[float, float] | None: if not arrays: return None pm = np.concatenate(arrays) - d = pm.max() - pm.min() + d = max(float(pm.max() - pm.min()), PHASE_MIN_HALF_SPAN) mid = (pm.max() + pm.min()) / 2 - return mid - d - 1e-9, mid + d + 1e-9 + return mid - d, mid + d def _temporal_panel( diff --git a/croak/plotting/screen.py b/croak/plotting/screen.py index fbcc08a..ffb2214 100644 --- a/croak/plotting/screen.py +++ b/croak/plotting/screen.py @@ -750,10 +750,11 @@ def pulse_field_views( et = grid.ifft(ew) it = _unit(np.abs(et) ** 2) t = grid.t / 1e-15 + phase_t = _masked_phase(it, et) temporal = LineView( curves=( CurveView(t, it, "retrieved", "intensity"), - CurveView(t, _masked_phase(it, et), "phase", "phase", True), + CurveView(t, phase_t, "phase", "phase", True), ), title="Temporal", xlabel="Time (fs)", @@ -761,6 +762,7 @@ def pulse_field_views( right_label="Phase (rad)", xrange=_zoom(t, it > PULSE_FRACTION), yrange=(0.0, 1.05), + right_range=_phase_ylim(phase_t), ) iw = _unit(np.abs(ew) ** 2) omega_abs = grid.omega + omega0 @@ -768,10 +770,11 @@ def pulse_field_views( lam = wlfreq(omega_abs[pos]) / 1e-9 order = np.argsort(lam) lam, ip = lam[order], iw[pos][order] + phase_w = _masked_phase(iw, ew)[pos][order] spectral = LineView( curves=( CurveView(lam, ip, "retrieved", "intensity"), - CurveView(lam, _masked_phase(iw, ew)[pos][order], "phase", "phase", True), + CurveView(lam, phase_w, "phase", "phase", True), ), title="Spectral", xlabel="Wavelength (nm)", @@ -779,6 +782,7 @@ def pulse_field_views( right_label="Phase (rad)", xrange=_zoom(lam, ip > PULSE_FRACTION), yrange=(0.0, 1.05), + right_range=_phase_ylim(phase_w), ) return temporal, spectral, _gabor(grid, et, omega0) diff --git a/tests/gui/test_stage_synthetic.py b/tests/gui/test_stage_synthetic.py index 6a0315e..9c5283f 100644 --- a/tests/gui/test_stage_synthetic.py +++ b/tests/gui/test_stage_synthetic.py @@ -162,6 +162,26 @@ def test_additive_noise_alone(stage): assert noisy.min() >= 0.0 and noisy.max() == pytest.approx(1.0) +def test_preview_draws_the_trace_continue_hands_on(stage): + stage.on_enter() + shown = stage.views["trace"].plot + stage.advance() + lam_nm = stage.state.load_data["lam"] / 1e-9 + # The λ window's own samples, not the whole simulation grid, in which the + # signal band was one or two rows of a featureless stripe. + assert shown.data.shape == stage.state.load_data["trace"].shape + assert shown.y_axis[0] == pytest.approx(lam_nm[0]) + assert shown.y_axis[-1] == pytest.approx(lam_nm[-1]) + + +def test_preview_with_an_empty_window_asks_to_widen_it(stage): + stage.interaction.setCurrentText("pg") + stage._wl = (100.0, 150.0) # below every wavelength the grid represents + stage.on_enter() + assert "widen the range" in stage.status_label.text() + assert stage.status_label.state == "warn" + + def test_preview_draws_the_four_panels(stage): stage.on_enter() titles = {view.title.text() for view in stage.views.values()} diff --git a/tests/test_plotting.py b/tests/test_plotting.py index a8662a0..1ad956f 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -848,6 +848,15 @@ def test_phase_ylim_of_nothing_finite_is_none(): assert plotting.pulse._phase_ylim(None, np.array([np.nan]), np.array([])) is None +def test_phase_ylim_keeps_a_flat_phase_from_filling_the_axis(): + floor = plotting.pulse.PHASE_MIN_HALF_SPAN + # Rounding noise of a transform-limited pulse: the axis stays ±floor wide. + low, high = plotting.pulse._phase_ylim(np.array([0.0, 1e-15, -2e-15])) + assert high - low == pytest.approx(2 * floor) + # A real phase keeps its own span as margin either side. + assert plotting.pulse._phase_ylim(np.array([0.0, 4.0])) == (-2.0, 6.0) + + def test_plot_residual_and_spectrogram_defaults(result_and_trace): import matplotlib.pyplot as plt diff --git a/tests/test_plotting_screen.py b/tests/test_plotting_screen.py index 96245af..4ac690e 100644 --- a/tests/test_plotting_screen.py +++ b/tests/test_plotting_screen.py @@ -202,6 +202,15 @@ def test_screen_pulse_views_zoom_to_the_pulse_and_blank_its_wings(): assert gram.yrange is not None +def test_screen_pulse_views_give_a_flat_phase_a_readable_axis(): + g = Grid(128, dt=0.5e-15) + ew = gaussian_pulse(g, 7e-15) # transform-limited: phase is rounding noise + temporal, spectral, _ = screen.pulse_field_views(g, ew, float(wlfreq(800e-9))) + for view in (temporal, spectral): + low, high = view.right_range + assert high - low >= 2.0 # ±1 rad at least, not ±1e-15 + + def test_screen_pulse_views_tolerate_an_empty_field(): g = Grid(64, dt=1e-15) temporal, spectral, gram = screen.pulse_field_views( From 8db0f27c55bc9116cf07963649f3b41a433c51c4 Mon Sep 17 00:00:00 2001 From: John Travers Date: Fri, 6 Nov 2026 09:59:00 +0000 Subject: [PATCH 24/41] Give the plots the width: a fixed, narrow form column The form took two fifths of the window. It is now 340 px beside the plots (the whole width when stacked on a narrow window), and its combo boxes shrink to the column instead of clipping. Co-Authored-By: Claude Opus 5.5 --- croak/gui/sheet.py | 37 ++++++++++++++++++++++++++++++++----- tests/gui/test_sheet.py | 22 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/croak/gui/sheet.py b/croak/gui/sheet.py index e1bf77f..5839639 100644 --- a/croak/gui/sheet.py +++ b/croak/gui/sheet.py @@ -22,6 +22,7 @@ QApplication, QBoxLayout, QCheckBox, + QComboBox, QFormLayout, QFrame, QGridLayout, @@ -54,11 +55,19 @@ "row_text", ] +COMBO_MIN_CHARS = 6 +"""The characters a combo box keeps when the form column squeezes it.""" + +# Qt's QWIDGETSIZE_MAX, the "no limit" of setMaximumWidth; PySide6 does not +# export the constant. +_NO_LIMIT = 16_777_215 + NARROW_WIDTH = 1000 """Below this window width (logical px) a sheet's plots move under its form.""" -FORM_MIN_WIDTH = 360 -"""The narrowest the form column may be, logical px.""" +FORM_WIDTH = 340 +"""The form column's width beside the plots, logical px: room for a label and +its editor, and no more, so the plots get the rest of the window.""" def plain(text: str) -> str: @@ -199,7 +208,7 @@ class Form(QFrame): def __init__(self, *, find: bool = False) -> None: super().__init__() self.setObjectName("form") - self.setMinimumWidth(FORM_MIN_WIDTH) + self.setMinimumWidth(FORM_WIDTH) outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) outer.setSpacing(0) @@ -268,6 +277,20 @@ def _focus_moved(self, _old: QWidget | None, new: QWidget | None) -> None: return widget = widget.parentWidget() + def showEvent(self, event) -> None: # noqa: N802, ANN001 — Qt override + """Let every combo box shrink to the column. + + A combo is as wide as its longest item by default, which would push the + fixed-width column into clipping; elided to the column, the full text + is in its drop-down. Done on show, so it covers combos filled late. + """ + for box in self.findChildren(QComboBox): + box.setSizeAdjustPolicy( + QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon + ) + box.setMinimumContentsLength(COMBO_MIN_CHARS) + super().showEvent(event) + def filter(self, query: str) -> None: """Show only the rows whose label or help contains ``query``.""" query = query.strip() @@ -287,8 +310,9 @@ def __init__(self, form: Form, plots: QWidget) -> None: self.split = QBoxLayout(QBoxLayout.Direction.LeftToRight, self) self.split.setContentsMargins(0, 0, 0, 0) self.split.setSpacing(0) - self.split.addWidget(form, stretch=2) - self.split.addWidget(plots, stretch=3) + self.split.addWidget(form) + self.split.addWidget(plots, stretch=1) + form.setMaximumWidth(FORM_WIDTH) @property def narrow(self) -> bool: @@ -303,6 +327,9 @@ def resizeEvent(self, event) -> None: # noqa: N802, ANN001 — Qt override if narrow else QBoxLayout.Direction.LeftToRight ) + # Stacked, the form spans the window; beside the plots it keeps + # its fixed width. + self.form.setMaximumWidth(_NO_LIMIT if narrow else FORM_WIDTH) super().resizeEvent(event) diff --git a/tests/gui/test_sheet.py b/tests/gui/test_sheet.py index 9099cb5..a113ca9 100644 --- a/tests/gui/test_sheet.py +++ b/tests/gui/test_sheet.py @@ -230,3 +230,25 @@ def test_sheet_progress_strip_clamps_its_bar(qtbot): strip.show_progress("Retrieving", -1.0, "") assert strip.bar.value() == 0 assert strip.state.text() == "Retrieving" + + +def test_sheet_combo_boxes_shrink_to_the_column(qtbot): + from PySide6.QtWidgets import QComboBox + + from croak.gui.sheet import FORM_WIDTH + + form, plots = _form(qtbot), QWidget() + long = QComboBox() + long.addItems(["Automatic (centroid phase) from a very long description"]) + form.add_section("Mode").form.addRow("Initial guess", long) + sheet = Sheet(form, plots) + qtbot.addWidget(sheet) + sheet.resize(NARROW_WIDTH + 400, 700) + sheet.show() + qtbot.waitExposed(sheet) + assert long.sizeAdjustPolicy() == ( + QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon + ) + assert form.width() == FORM_WIDTH # the long item does not widen the column + sheet.resize(NARROW_WIDTH - 200, 700) # stacked: the form spans the sheet + qtbot.waitUntil(lambda: form.width() > FORM_WIDTH) From 6ee6415dfb86f5d50d036e93470c782c88c91e03 Mon Sep 17 00:00:00 2001 From: John Travers Date: Fri, 6 Nov 2026 09:59:00 +0000 Subject: [PATCH 25/41] Make Check a stage; add a Home button and an About page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The marginal check was a second step inside Data, reached by Continue or a segment switch. It is now a stage of its own in the top bar, open as soon as a trace is loaded, and a loader that hands its trace on moves the window there. Home leads the stage row with an icon, and About (Help menu, ⋯ and Home) gives the version, links to the manual, GitHub, PyPI and issues, the authors and the copyright. Co-Authored-By: Claude Opus 5.5 --- croak/gui/__init__.py | 2 +- croak/gui/about.py | 105 +++++++++++++++++++++++++++++ croak/gui/assets/home.svg | 1 + croak/gui/home.py | 6 ++ croak/gui/pages.py | 110 ++++++++----------------------- croak/gui/stage.py | 6 +- croak/gui/theme.py | 1 + croak/gui/window.py | 56 ++++++++++++---- croak/gui/workflow.py | 25 +++++-- tests/gui/gui_session.py | 20 +++--- tests/gui/test_about.py | 40 +++++++++++ tests/gui/test_window.py | 40 ++++++++++- tests/gui/test_window_session.py | 46 +++++++------ tests/gui/test_wizard_flow.py | 12 ++-- tests/gui/test_workflow.py | 19 +++++- 15 files changed, 343 insertions(+), 146 deletions(-) create mode 100644 croak/gui/about.py create mode 100644 croak/gui/assets/home.svg create mode 100644 tests/gui/test_about.py diff --git a/croak/gui/__init__.py b/croak/gui/__init__.py index 67945f9..0cc67d1 100644 --- a/croak/gui/__init__.py +++ b/croak/gui/__init__.py @@ -3,7 +3,7 @@ The GUI is a thin layer over the croak library: it loads, cleans, retrieves, post-processes, tunes dispersion, estimates uncertainty, plots and saves — all through the library's functions and the Qt-free :mod:`croak.session`. Its window -takes a session from Home through Data, Prepare, Retrieve and Analyse. +takes a session from Home through Data, Check, Prepare, Retrieve and Analyse. Launch with the ``croak-gui`` command, :func:`run_gui`, or ``python -m croak.gui``. """ diff --git a/croak/gui/about.py b/croak/gui/about.py new file mode 100644 index 0000000..9d0d500 --- /dev/null +++ b/croak/gui/about.py @@ -0,0 +1,105 @@ +"""About: what croak is, where it lives, who wrote it and under what licence. + +Opened from Help ▸ About croak, the ⋯ menu and Home. The version, summary, +authors and links come from the installed package's metadata, so they cannot +drift from ``pyproject.toml``; only the copyright line and the PyPI address, +which the metadata does not carry, are written here. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from importlib import metadata + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget + +from .theme import PALETTE, label + +__all__ = ["COPYRIGHT", "PYPI_URL", "About", "project_links"] + +COPYRIGHT = "Copyright © 2026 John C. Travers and contributors" +"""The copyright line, as in the ``LICENSE`` file.""" + +PYPI_URL = "https://pypi.org/project/croak/" +"""The package's page on PyPI.""" + +ABOUT_WIDTH = 640 +"""The About text's measure, logical px: a comfortable line to read.""" + +LICENCE_NOTE = ( + "croak is free software under the MIT licence. Its window is set in IBM " + "Plex Sans, under the SIL Open Font License 1.1." +) + + +def project_links(entries: Sequence[str] | None = None) -> dict[str, str]: + """Return the project's links by name: the manual, GitHub, PyPI and issues. + + Parameters + ---------- + entries : sequence of str, optional + ``Project-URL`` metadata entries, each ``"Name, url"``; the installed + croak's by default. A link the entries lack is left out (PyPI's is + always there). + + Examples + -------- + >>> project_links(["Documentation, https://croak.readthedocs.io"]) + {'Manual': 'https://croak.readthedocs.io', 'Package on PyPI': 'https://pypi.org/project/croak/'} + """ # noqa: E501 — a doctest's output must stay on one line + if entries is None: + entries = metadata.metadata("croak").get_all("Project-URL") or [] + urls = dict(entry.split(", ", 1) for entry in entries) + links = { + "Manual": urls.get("Documentation", ""), + "Source on GitHub": urls.get("Repository", ""), + "Package on PyPI": PYPI_URL, + "Report an issue": urls.get("Issues", ""), + } + return {name: url for name, url in links.items() if url} + + +def _link(text: str, url: str) -> QLabel: + # Qt draws links in the system's blue; the palette's accent matches the window. + style = f"color: {PALETTE.accent}; text-decoration: none;" + widget = label(f'{text}', "aboutLink") + widget.setTextFormat(Qt.TextFormat.RichText) + widget.setOpenExternalLinks(True) + widget.setToolTip(url) + return widget + + +class About(QWidget): + """The About page.""" + + def __init__(self) -> None: + super().__init__() + self.setObjectName("page") + meta = metadata.metadata("croak") + outer = QVBoxLayout(self) + outer.setContentsMargins(40, 36, 40, 24) + body = QWidget() + body.setMaximumWidth(ABOUT_WIDTH) + outer.addWidget(body) + outer.addStretch(1) + column = QVBoxLayout(body) + column.setContentsMargins(0, 0, 0, 0) + column.setSpacing(10) + column.addWidget(label("croak", "h1")) + self.version = label(f"Version {meta['Version']}", "note") + column.addWidget(self.version) + column.addWidget(label(meta["Summary"], "", wrap=True)) + column.addSpacing(8) + column.addWidget(label("Links", "h3")) + self.links = project_links() + for name, url in self.links.items(): + column.addWidget(_link(name, url)) + column.addSpacing(8) + column.addWidget(label("Authors", "h3")) + authors = meta["Author"] or "" + self.authors = label(authors, "", wrap=True) + column.addWidget(self.authors) + column.addSpacing(8) + column.addWidget(label(COPYRIGHT, "note")) + column.addWidget(label(LICENCE_NOTE, "note", wrap=True)) diff --git a/croak/gui/assets/home.svg b/croak/gui/assets/home.svg new file mode 100644 index 0000000..4105606 --- /dev/null +++ b/croak/gui/assets/home.svg @@ -0,0 +1 @@ + diff --git a/croak/gui/home.py b/croak/gui/home.py index 277a81c..4f5380a 100644 --- a/croak/gui/home.py +++ b/croak/gui/home.py @@ -99,10 +99,13 @@ class Home(QWidget): A tile was chosen: the entry to start a session from. open_session : Signal(str) A session to reopen: an ``options.toml`` path, or ``""`` to ask for one. + about : Signal + About croak was asked for. """ start = Signal(str) open_session = Signal(str) + about = Signal() def __init__(self) -> None: super().__init__() @@ -133,6 +136,9 @@ def __init__(self) -> None: self.open_button.clicked.connect(lambda: self.open_session.emit("")) actions.addWidget(self.open_button) actions.addStretch(1) + self.about_button = button("About croak", "link") + self.about_button.clicked.connect(self.about) + actions.addWidget(self.about_button) main.addLayout(actions) main.addStretch(1) columns.addLayout(main, stretch=3) diff --git a/croak/gui/pages.py b/croak/gui/pages.py index 50ad934..1b9e53e 100644 --- a/croak/gui/pages.py +++ b/croak/gui/pages.py @@ -1,11 +1,11 @@ -"""The window's four pages for a session, and the pages made of other pages. - -:func:`default_pages` builds a session's Data, Prepare, Retrieve and Analyse -pages from the shared state. Data is two steps on one page: the entry's own -loader (a measured trace, a simulated scan or a synthetic pulse) and then the -marginal check of the loaded trace. Analyse is a rail of pages that read the -retrieved result: its panels (Overview, Traces, Pulse, Diagnostics), then -Dispersion and Uncertainty. +"""The window's pages for a session, and the pages made of other pages. + +:func:`default_pages` builds a session's Data, Check, Prepare, Retrieve and +Analyse pages from the shared state. Data holds the entry's own loader (a +measured trace, a simulated scan or a synthetic pulse) and brings a reopened +session's trace back; Check is the marginal check of what it loaded. Analyse is +a rail of pages that read the retrieved result: its panels (Overview, Traces, +Pulse, Diagnostics), then Dispersion and Uncertainty. """ from __future__ import annotations @@ -30,7 +30,6 @@ from .base import Stage from .help import HelpDialog from .retrieval_views import ResultPage -from .sheet import Segments from .stage import PageBase from .stage_dispersion import StageDispersion from .stage_load import StageLoad, compute_preproc_defaults, trace_signature @@ -49,15 +48,9 @@ RAIL_WIDTH = 170 """The width of Analyse's rail, logical px.""" -_ENTRY_STEP = { - "experimental": "Load the trace", - "simulated": "Load the scan", - "synthetic": "Generate the trace", -} - class DataPage(PageBase): - """Data: the entry's loader, then the marginal check of what it loaded. + """Data: the entry's loader, which hands a trace on to the check. Parameters ---------- @@ -80,91 +73,41 @@ def __init__( self.entry_page = StageSimulated(state, adopt_geometry) else: self.entry_page = StageLoad(state) - self.check = StageMarginalCheck(state) - self._went = "" - head = frame("strip") - hl = QHBoxLayout(head) - hl.setContentsMargins(16, 8, 16, 8) - self.steps = Segments( - [("entry", _ENTRY_STEP[entry]), ("check", "Check the marginal")], - "entry", - ) - self.steps.changed.connect(self.show_step) - hl.addWidget(self.steps) - hl.addStretch(1) - self.stack = QStackedWidget() - self.stack.addWidget(self.entry_page) - self.stack.addWidget(self.check) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(head) - layout.addWidget(self.stack, stretch=1) - for page in (self.entry_page, self.check): - page.changed.connect(self.changed) - page.message.connect(self.message) - self.entry_page.advanced.connect(self._advanced) - state.load_data_changed.connect(self._sync) - self._sync() - - @property - def step(self) -> str: - """The step on screen: ``"entry"`` or ``"check"``.""" - return self.steps.value - - def current(self) -> Stage: - """The step's page.""" - return self.check if self.step == "check" else self.entry_page - - def _sync(self) -> None: - """Offer the check only once a trace is loaded.""" - self.steps.buttons["check"].setEnabled(self.state.load_data is not None) - self.changed.emit() - - def show_step(self, step: str) -> None: - """Show ``step`` (``"entry"`` or ``"check"``) and refresh it.""" - self.steps.set_value(step) - self.stack.setCurrentWidget(self.current()) - self.current().on_enter() - self.changed.emit() - - def _advanced(self, where: str) -> None: - self._went = where - if where == "check": - self.show_step("check") + layout.addWidget(self.entry_page) + self.entry_page.changed.connect(self.changed) + self.entry_page.message.connect(self.message) + # Wherever the loader hands its trace on to, the window's route rules + # know (a raw simulated scan skips to Retrieve). + self.entry_page.advanced.connect(lambda _where: self.moved_on.emit()) # -- hooks the window reads --------------------------------------------- def on_enter(self) -> None: - self.current().on_enter() - - def page_key(self) -> str: - return self.step + self.entry_page.on_enter() def can_advance(self) -> bool: - if self.step == "check" or self.state.entry == "experimental": + if self.state.entry == "experimental": return self.state.load_data is not None return self.entry_page.can_advance() def act(self, kind: str) -> bool: - """Continue: from the loader to the check, from the check onwards. + """Continue: hand the trace on to the next stage. - A measured trace is loaded by its own button, so its Continue only moves - to the check; the synthetic and simulated loaders build the trace here. - A raw simulated scan goes on to Retrieve straight from the loader. + A measured trace is loaded by its own button, so its Continue just + moves on. The synthetic and simulated loaders build the trace here and, + if they could, say so through :attr:`moved_on`; if not, the page stays + and the loader says why. """ if kind != "continue": return super().act(kind) - if self.step == "check": - return True if self.state.entry == "experimental": - self.show_step("check") - return False - self._went = "" + return True self.entry_page.advance() - return self._went == "retrieve" + return False def show_help(self) -> HelpDialog | None: - return self.current().show_help() + return self.entry_page.show_help() # -- session replay ----------------------------------------------------- def restore(self) -> bool: @@ -317,7 +260,7 @@ def show_help(self) -> HelpDialog | None: def default_pages(state: WizardState) -> Mapping[Place, PageBase]: - """Build a session's Data, Prepare, Retrieve and Analyse pages.""" + """Build a session's Data, Check, Prepare, Retrieve and Analyse pages.""" retrieve = StageRetrieve(state) results: list[tuple[str, str, PageBase]] = [ (key, title, ResultPage(state, key, retrieve.overview)) @@ -341,6 +284,7 @@ def default_pages(state: WizardState) -> Mapping[Place, PageBase]: Place.DATA: DataPage( state, lambda geom: retrieve.apply_simulated_geometry(geom) ), + Place.CHECK: StageMarginalCheck(state), Place.PREPARE: StagePreprocess(state), Place.RETRIEVE: retrieve, Place.ANALYSE: analyse, diff --git a/croak/gui/stage.py b/croak/gui/stage.py index 044e6ad..5e00268 100644 --- a/croak/gui/stage.py +++ b/croak/gui/stage.py @@ -5,7 +5,7 @@ Most pages are a :class:`StagePage`: a :class:`~croak.gui.sheet.Sheet` with a form of folding sections on the left and, on the right, an advice box, a progress strip, the derived numbers and the stage's plots. Pages made of other pages -(Data's steps, Analyse's rail) build on :class:`PageBase` directly. +(Data's loader, Analyse's rail) build on :class:`PageBase` directly. """ from __future__ import annotations @@ -35,6 +35,9 @@ class PageBase(QWidget): started or ended), so the window re-reads the hooks. message : Signal(str) A passing notice for the window's status bar. + moved_on : Signal + The page handed its work on by itself (an entry loader's own forward + action did), so the window goes to the next stage. """ HELP_TITLE: str = "" @@ -46,6 +49,7 @@ class PageBase(QWidget): changed = Signal() message = Signal(str) + moved_on = Signal() def __init__(self, state: WizardState) -> None: super().__init__() diff --git a/croak/gui/theme.py b/croak/gui/theme.py index 7206012..3c39d05 100644 --- a/croak/gui/theme.py +++ b/croak/gui/theme.py @@ -143,6 +143,7 @@ def stylesheet(p: Palette = PALETTE) -> str: f"QPushButton#brand {{ border: 0; background: none; padding: 4px;" f" font-size: {H2_PX}px; font-weight: 600; color: {p.ink}; }}", f"QLabel#context {{ color: {p.muted}; }}", + f"QFrame#divider {{ background: {p.hairline}; }}", f"QPushButton#stage {{ {flat} padding: 6px 14px; color: {p.muted};" f" font-weight: 500; }}", f"QPushButton#stage:hover {{ background: {p.hover_bg}; }}", diff --git a/croak/gui/window.py b/croak/gui/window.py index 378f9d8..93c637a 100644 --- a/croak/gui/window.py +++ b/croak/gui/window.py @@ -1,6 +1,6 @@ -"""The workflow window: Home, then Data, Prepare, Retrieve and Analyse. +"""The workflow window: Home, then Data, Check, Prepare, Retrieve and Analyse. -The top bar carries the brand (back to Home), what the session is, the four +The top bar carries the brand (back to Home), what the session is, the five stage buttons, the one primary action and an overflow menu (⋯). Which stages open and what the primary action does are the pure rules of :mod:`croak.gui.workflow`; this module applies them to widgets. A drawer on the @@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, cast from PySide6.QtCore import Qt -from PySide6.QtGui import QAction, QCloseEvent, QKeySequence +from PySide6.QtGui import QAction, QCloseEvent, QIcon, QKeySequence from PySide6.QtWidgets import ( QApplication, QFileDialog, @@ -39,11 +39,12 @@ from ..session import files as session_files from ..session import retarget from . import settings +from .about import About from .branding import app_icon from .home import ENTRY_TILES, Home from .stage import PageBase from .state import WizardState -from .theme import button, frame, label, load_fonts, set_flag, stylesheet +from .theme import ASSETS, button, frame, label, load_fonts, set_flag, stylesheet from .workflow import ( STAGES, Action, @@ -62,7 +63,7 @@ __all__ = ["DRAWER_WIDTH", "MainWindow", "PageFactory"] PageFactory = Callable[[WizardState], Mapping[Stage, PageBase]] -"""Builds a session's four stage pages from the shared state.""" +"""Builds a session's stage pages from the shared state.""" DRAWER_WIDTH = 440 """The width of the settings drawer, logical px.""" @@ -80,7 +81,7 @@ class MainWindow(QMainWindow): Parameters ---------- make_pages : PageFactory, optional - Builds the Data, Prepare, Retrieve and Analyse pages for a session + Builds the Data, Check, Prepare, Retrieve and Analyse pages for a session (:func:`croak.gui.pages.default_pages` by default). state : WizardState, optional The shared session state (a fresh one by default). @@ -118,7 +119,10 @@ def __init__( self.home = Home() self.home.start.connect(self.new_session) self.home.open_session.connect(self.open_session) + self.home.about.connect(self.show_about) self.stack.addWidget(self.home) + self.about = About() + self.stack.addWidget(self.about) middle.addWidget(self.stack, stretch=1) middle.addWidget(self._make_drawer()) column.addLayout(middle, stretch=1) @@ -152,6 +156,17 @@ def _make_top(self) -> QFrame: bar.addWidget(self.brand) bar.addWidget(self.context) bar.addStretch(1) + # Home leads the stage row, where navigation is looked for; the brand + # also goes Home, but nobody guesses that a name is a button. + self.home_button = button("Home", "stage") + self.home_button.setIcon(QIcon(str(ASSETS / "home.svg"))) + self.home_button.setToolTip("Home: start or reopen a session") + self.home_button.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.home_button.clicked.connect(self.go_home) + bar.addWidget(self.home_button) + divider = frame("divider") + divider.setFixedSize(1, 20) + bar.addWidget(divider) self.stage_buttons: dict[Stage, QPushButton] = {} for stage in STAGES: b = button(stage.title, "stage") @@ -226,7 +241,10 @@ def _make_menus(self) -> None: "help": self._action( "Help for this stage", self.show_help, QKeySequence(keys.HelpContents) ), + "about": self._action("About croak", self.show_about), } + # On macOS, Qt moves it to the application menu, where About belongs. + self.actions_["about"].setMenuRole(QAction.MenuRole.AboutRole) overflow = QMenu(self) for key in ("save", "open"): overflow.addAction(self.actions_[key]) @@ -235,6 +253,7 @@ def _make_menus(self) -> None: overflow.addAction(self.actions_[key]) overflow.addSeparator() overflow.addAction(self.actions_["help"]) + overflow.addAction(self.actions_["about"]) self.overflow.setMenu(overflow) bar = self.menuBar() file_menu = bar.addMenu("&File") @@ -243,7 +262,9 @@ def _make_menus(self) -> None: view_menu = bar.addMenu("&View") for key in ("options", "python"): view_menu.addAction(self.actions_[key]) - bar.addMenu("&Help").addAction(self.actions_["help"]) + help_menu = bar.addMenu("&Help") + for key in ("help", "about"): + help_menu.addAction(self.actions_[key]) def _build_pages(self) -> None: """Replace the stage pages with fresh ones bound to the state's parameters.""" @@ -255,10 +276,11 @@ def _build_pages(self) -> None: if missing: names = ", ".join(sorted(s.value for s in missing)) raise ValueError(f"the page factory built no page for {names}") - for page in self.pages.values(): + for stage, page in self.pages.items(): self.stack.addWidget(page) page.changed.connect(self.refresh) page.message.connect(self.notify) + page.moved_on.connect(lambda stage=stage: self.move_on(stage)) # -- the route ------------------------------------------------------------ def progress(self) -> Progress: @@ -287,13 +309,14 @@ def current_action(self) -> Action: def refresh(self) -> None: """Re-apply the route rules to the top bar.""" progress = self.progress() - in_session = self.stage is not Stage.HOME or bool(self.pages) + in_session = bool(self.pages) for stage, b in self.stage_buttons.items(): reason = blocked_reason(stage, progress) if self.pages else "no session" b.setVisible(in_session) b.setEnabled(not reason) b.setToolTip(reason) set_flag(b, "current", stage is self.stage) + set_flag(self.home_button, "current", self.stage is Stage.HOME) action = self.current_action() self.primary.setVisible(bool(action.kind)) self.primary.setText(action.text) @@ -307,8 +330,9 @@ def refresh(self) -> None: def go(self, stage: Stage) -> bool: """Show ``stage`` if it can be opened; say why not otherwise.""" - if stage is Stage.HOME or not self.pages: - reason = "" if stage is Stage.HOME else "no session" + outside = stage in (Stage.HOME, Stage.ABOUT) + if outside or not self.pages: + reason = "" if outside else "no session" else: reason = blocked_reason(stage, self.progress()) if reason: @@ -318,6 +342,8 @@ def go(self, stage: Stage) -> bool: if stage is Stage.HOME: self.home.show_recent(settings.recent_sessions()) self.stack.setCurrentWidget(self.home) + elif stage is Stage.ABOUT: + self.stack.setCurrentWidget(self.about) else: page = self.pages[stage] self.stack.setCurrentWidget(page) @@ -333,6 +359,10 @@ def go_home(self) -> None: """Show Home (the session stays open until a new one starts).""" self.show_home() + def show_about(self) -> None: + """Show About, keeping any open session.""" + self.go(Stage.ABOUT) + def do_primary(self) -> None: """Do what the primary action says.""" action = self.current_action() @@ -346,6 +376,10 @@ def do_primary(self) -> None: self.go(next_stage(self.stage, self.progress())) self.refresh() + def move_on(self, stage: Stage) -> None: + """Go to the stage after ``stage``, whose page handed its work on.""" + self.go(next_stage(stage, self.progress())) + # -- sessions --------------------------------------------------------------- def confirm_discard(self) -> bool: """Ask before a new session replaces one holding data; True to go ahead.""" diff --git a/croak/gui/workflow.py b/croak/gui/workflow.py index e27201c..3372e30 100644 --- a/croak/gui/workflow.py +++ b/croak/gui/workflow.py @@ -1,6 +1,6 @@ """The window's route through a session, as rules without Qt. -A session runs Home → Data → Prepare → Retrieve → Analyse. Which stages can be +A session runs Home → Data → Check → Prepare → Retrieve → Analyse. Which stages can be opened, and what the one primary action in the top bar does, depend only on what the session holds (:class:`Progress`), so they are decided here, as pure functions the window applies and the tests check directly. @@ -31,7 +31,9 @@ class Stage(StrEnum): """A place in the window.""" HOME = "home" + ABOUT = "about" DATA = "data" + CHECK = "check" PREPARE = "prepare" RETRIEVE = "retrieve" ANALYSE = "analyse" @@ -42,8 +44,8 @@ def title(self) -> str: return self.value.capitalize() -STAGES = (Stage.DATA, Stage.PREPARE, Stage.RETRIEVE, Stage.ANALYSE) -"""The stages of a session, in order (Home is outside the session).""" +STAGES = (Stage.DATA, Stage.CHECK, Stage.PREPARE, Stage.RETRIEVE, Stage.ANALYSE) +"""The stages of a session, in order (Home and About are outside the session).""" @dataclass(frozen=True) @@ -92,12 +94,14 @@ def blocked_reason(stage: Stage, progress: Progress) -> str: >>> blocked_reason(Stage.DATA, Progress(entry="synthetic")) '' """ - if stage is Stage.HOME: + if stage in (Stage.HOME, Stage.ABOUT): return "" if not progress.entry: return "Start a session from Home first." if stage is Stage.DATA: return "" + if stage is Stage.CHECK: + return "" if progress.has_load_data else "Load a trace first." if stage is Stage.PREPARE: if progress.raw_direct: return ( @@ -113,15 +117,20 @@ def blocked_reason(stage: Stage, progress: Progress) -> str: def next_stage(stage: Stage, progress: Progress) -> Stage: """The stage "Continue" leads to from ``stage``. - From Data a raw simulated scan goes straight to Retrieve. + A raw simulated scan skips the check and Prepare: it goes from Data (or, + if checked after all, from Check) straight to Retrieve. Raises ------ ValueError For a stage with no "Continue" (Home, Retrieve, Analyse). """ + if stage in (Stage.DATA, Stage.CHECK) and progress.raw_direct: + return Stage.RETRIEVE if stage is Stage.DATA: - return Stage.RETRIEVE if progress.raw_direct else Stage.PREPARE + return Stage.CHECK + if stage is Stage.CHECK: + return Stage.PREPARE if stage is Stage.PREPARE: return Stage.RETRIEVE raise ValueError(f"{stage.title} has no next stage") @@ -157,10 +166,12 @@ def primary_action(stage: Stage, progress: Progress, page: str = "") -> Action: >>> primary_action(Stage.DATA, Progress(entry="synthetic")).enabled False """ - if stage is Stage.HOME: + if stage in (Stage.HOME, Stage.ABOUT): return Action("", "", enabled=False) if stage is Stage.DATA: return Action("continue", "Continue", enabled=progress.data_ready) + if stage is Stage.CHECK: + return Action("continue", "Continue", enabled=progress.has_load_data) if stage is Stage.PREPARE: return Action("continue", "Continue", enabled=progress.has_tracedata) if stage is Stage.RETRIEVE: diff --git a/tests/gui/gui_session.py b/tests/gui/gui_session.py index 064ba0b..1f4af3c 100644 --- a/tests/gui/gui_session.py +++ b/tests/gui/gui_session.py @@ -2,9 +2,9 @@ The GUI tests were written against six numbered stages (1 Load, 2 Marginal check, 3 Preprocess, 4 Retrieve, 5 Dispersion, 6 Uncertainty). The window -now has four places (Data with two steps, Prepare, Retrieve, Analyse with a -rail), so :class:`Driver` maps the numbers onto them and hands back the stage -pages, which keep their own behaviour. Navigation tests use the window itself. +now has five places (Data, Check, Prepare, Retrieve, Analyse with a rail), so +:class:`Driver` maps the numbers onto them and hands back the stage pages, +which keep their own behaviour. Navigation tests use the window itself. """ from __future__ import annotations @@ -17,8 +17,8 @@ # stage number -> (place, step or rail page) PLACES = { - 1: (Stage.DATA, "entry"), - 2: (Stage.DATA, "check"), + 1: (Stage.DATA, ""), + 2: (Stage.CHECK, ""), 3: (Stage.PREPARE, ""), 4: (Stage.RETRIEVE, ""), 5: (Stage.ANALYSE, "dispersion"), @@ -69,7 +69,7 @@ def stages(self) -> list: """The six stage pages in the old order (the entry's loader first).""" return [ self.data.entry_page, - self.data.check, + self.win.pages[Stage.CHECK], self.win.pages[Stage.PREPARE], self.win.pages[Stage.RETRIEVE], self.analyse.pages["dispersion"], @@ -94,9 +94,7 @@ def goto(self, number: int) -> None: win.stage = place page = win.pages[place] win.stack.setCurrentWidget(page) - if place is Stage.DATA: - self.data.show_step(sub) - elif place is Stage.ANALYSE: + if place is Stage.ANALYSE: self.analyse.show_page(sub) else: page.on_enter() @@ -109,15 +107,13 @@ def stage_number(self) -> int: for number, (where, sub) in PLACES.items(): if where is not place: continue - if place is Stage.DATA and sub != self.data.step: - continue if place is Stage.ANALYSE and sub != self.analyse.key: continue return number return 0 def current_page(self): - """The stage page on screen (inside Data's steps or Analyse's rail).""" + """The stage page on screen (inside Data or Analyse's rail).""" return self.stages[self.stage_number - 1] if self.stage_number else None def __getattr__(self, name: str): diff --git a/tests/gui/test_about.py b/tests/gui/test_about.py new file mode 100644 index 0000000..ae55609 --- /dev/null +++ b/tests/gui/test_about.py @@ -0,0 +1,40 @@ +"""The About page: version, links, authors, copyright.""" + +from __future__ import annotations + +from importlib import metadata +from pathlib import Path + +import pytest + +pytest.importorskip("PySide6") + +from croak.gui.about import COPYRIGHT, PYPI_URL, About, project_links + +ROOT = Path(__file__).resolve().parents[2] + + +def test_about_shows_the_installed_version_authors_and_links(qtbot): + about = About() + qtbot.addWidget(about) + meta = metadata.metadata("croak") + assert meta["Version"] in about.version.text() + assert about.authors.text() == meta["Author"] + assert about.links["Package on PyPI"] == PYPI_URL + assert set(about.links) == { + "Manual", + "Source on GitHub", + "Package on PyPI", + "Report an issue", + } + + +def test_about_links_leave_out_what_the_metadata_lacks(): + assert project_links([]) == {"Package on PyPI": PYPI_URL} + links = project_links(["Repository, https://example.org/croak"]) + assert links["Source on GitHub"] == "https://example.org/croak" + + +def test_about_copyright_matches_the_licence_file(): + licence = (ROOT / "LICENSE").read_text(encoding="utf-8") + assert COPYRIGHT.replace("©", "(c)") in licence diff --git a/tests/gui/test_window.py b/tests/gui/test_window.py index da00241..cb05279 100644 --- a/tests/gui/test_window.py +++ b/tests/gui/test_window.py @@ -174,9 +174,11 @@ def test_window_primary_action_continues_through_the_stages(window): window.do_primary() # disabled: does nothing assert data.acted == [] data.ready = True - window.refresh() + window.state.set_load_data({"trace": "loaded"}) window.do_primary() assert data.acted == ["continue"] + assert window.stage is Stage.CHECK + window.do_primary() # the check passes the trace on assert window.stage is Stage.PREPARE # Prepare cannot continue until a trace is prepared. assert not window.primary.isEnabled() @@ -187,6 +189,13 @@ def test_window_primary_action_continues_through_the_stages(window): assert window.primary.text() == "Retrieve" +def test_window_follows_a_page_that_moves_on_by_itself(window): + window.new_session("synthetic") + window.state.set_load_data({"trace": "generated"}) + window.pages[Stage.DATA].moved_on.emit() # e.g. the generator's own Generate + assert window.stage is Stage.CHECK + + def test_window_raw_simulated_scan_continues_straight_to_retrieve(window): window.new_session("simulated") window.state.simulated.raw_direct = True @@ -241,6 +250,31 @@ def test_window_brand_returns_home_keeping_the_session(window): assert window.go(Stage.DATA) +def test_window_home_button_returns_home_and_marks_it(window): + assert window.home_button.property("current") is True + window.new_session("synthetic") + assert window.home_button.property("current") is False + window.home_button.click() + assert window.stage is Stage.HOME and window.pages + assert window.home_button.property("current") is True + + +@pytest.mark.parametrize("route", ["menu", "home"]) +def test_window_about_opens_with_or_without_a_session(window, route): + if route == "menu": + window.actions_["about"].trigger() + else: + window.home.about_button.click() + assert window.stage is Stage.ABOUT + assert window.stack.currentWidget() is window.about + assert not window.primary.isVisible() + assert not any(b.isVisibleTo(window) for b in window.stage_buttons.values()) + window.new_session("synthetic") + window.show_about() # a session stays open behind About + assert window.stage is Stage.ABOUT + assert window.go(Stage.DATA) + + def test_window_new_session_asks_before_discarding_data(window, monkeypatch): window.new_session("experimental") window.state.set_load_data({"trace": 1}) @@ -258,7 +292,9 @@ def partial(state): win = MainWindow(partial) qtbot.addWidget(win) - with pytest.raises(ValueError, match="no page for analyse, prepare, retrieve"): + with pytest.raises( + ValueError, match="no page for analyse, check, prepare, retrieve" + ): win.new_session("synthetic") diff --git a/tests/gui/test_window_session.py b/tests/gui/test_window_session.py index e8ff49b..678bf26 100644 --- a/tests/gui/test_window_session.py +++ b/tests/gui/test_window_session.py @@ -37,41 +37,38 @@ def _point_at_experiment(state, experiment_h5, experiment): p.interaction = experiment.interaction -# -- Data's two steps ------------------------------------------------------------ +# -- Data and Check -------------------------------------------------------------- def test_window_data_waits_for_a_loaded_trace_then_checks_it( qtbot, experiment_h5, experiment ): d = Driver(qtbot, "experimental") win = d.win - assert d.data.step == "entry" assert not win.primary.isEnabled() # nothing loaded yet - assert not d.data.steps.buttons["check"].isEnabled() + assert not win.stage_buttons[Stage.CHECK].isEnabled() _point_at_experiment(win.state, experiment_h5, experiment) d.stages[0]._load_preview() assert win.primary.isEnabled() - assert d.data.steps.buttons["check"].isEnabled() - win.do_primary() # a measured trace: on to the check, still on Data - assert win.stage is Stage.DATA and d.data.step == "check" - assert d.current_page() is d.data.check + assert win.stage_buttons[Stage.CHECK].isEnabled() # no Continue needed + win.do_primary() # a measured trace: on to the check + assert win.stage is Stage.CHECK + assert d.current_page() is win.pages[Stage.CHECK] win.do_primary() # from the check, on to Prepare assert win.stage is Stage.PREPARE assert win.state.tracedata is not None -def test_window_data_steps_can_be_chosen_directly(qtbot): +def test_window_data_and_check_answer_for_their_own_page(qtbot): d = Driver(qtbot, "synthetic") - d.synthetic.advance() # generated: the generator moves to the check - assert d.data.step == "check" and d.data.page_key() == "check" - d.data.steps.buttons["entry"].click() - assert d.data.step == "entry" - d.data.on_enter() d.data.show_help() # the generator has no help of its own: nothing opens - d.data.steps.buttons["check"].click() - dialog = d.data.show_help() - assert dialog is not None - dialog.close() with pytest.raises(ValueError, match="no 'retrieve' action"): d.data.act("retrieve") + d.win.do_primary() # generated: the check opens + assert d.win.stage is Stage.CHECK + dialog = d.win.pages[Stage.CHECK].show_help() + assert dialog is not None + dialog.close() + assert d.win.go(Stage.DATA) # back to the generator, the trace kept + assert d.state.load_data is not None def test_window_synthetic_continue_generates_and_checks(qtbot): @@ -79,14 +76,25 @@ def test_window_synthetic_continue_generates_and_checks(qtbot): assert d.win.primary.isEnabled() # a synthetic pulse is ready at once d.win.do_primary() assert d.state.load_data is not None - assert d.win.stage is Stage.DATA and d.data.step == "check" + assert d.win.stage is Stage.CHECK + + +def test_window_synthetic_continue_stays_when_generation_fails(qtbot, monkeypatch): + d = Driver(qtbot, "synthetic") + + def fail(): + raise ValueError("bad grid") + + monkeypatch.setattr(d.synthetic, "_pulse", fail) + d.win.do_primary() + assert d.win.stage is Stage.DATA and d.state.load_data is None def test_window_simulated_waits_for_a_previewed_scan(qtbot, simulated_scan_h5): d = Driver(qtbot, "simulated") assert not d.win.primary.isEnabled() d.win.do_primary() # disabled: nothing happens - assert d.data.step == "entry" + assert d.win.stage is Stage.DATA d.simulated.path_edit.setText(simulated_scan_h5) d.simulated._populate_file_options(simulated_scan_h5) d.simulated._preview() diff --git a/tests/gui/test_wizard_flow.py b/tests/gui/test_wizard_flow.py index 66332b4..674b6e5 100644 --- a/tests/gui/test_wizard_flow.py +++ b/tests/gui/test_wizard_flow.py @@ -134,9 +134,7 @@ def test_synthetic_back_returns_to_generator(qtbot): assert w.stage_number == 2 # marginal check w.goto(3) # advance to preprocess - w.win.stage_buttons[Stage.DATA].click() # back to Data, on its check step - assert w.current_page() is w.stages[1] - w.data.steps.buttons["entry"].click() # -> the synthetic generator + w.win.stage_buttons[Stage.DATA].click() # back to Data: the generator assert w.current_page() is w.synthetic # the in-progress data survives the round trip (no reset on Back) assert w.state.truth is not None @@ -376,9 +374,7 @@ def test_simulated_back_returns_to_loader(qtbot, simulated_scan_h5, simulated_tr assert w.stage_number == 2 w.goto(3) # advance to preprocess - w.win.stage_buttons[Stage.DATA].click() # back to Data, on its check step - assert w.current_page() is w.stages[1] - w.data.steps.buttons["entry"].click() # -> the simulated loader + w.win.stage_buttons[Stage.DATA].click() # back to Data: the simulated loader assert w.current_page() is w.simulated @@ -1705,14 +1701,14 @@ def test_simulated_reload_keeps_preproc_settings(qtbot, simulated_scan_h5): w.state.preproc.use_threshold = True # Back to the loader and forward again with the same options -> settings kept - w.data.steps.buttons["entry"].click() # back to the simulated loader + w.win.stage_buttons[Stage.DATA].click() # back to the simulated loader assert w.current_page() is w.simulated sim.advance() assert w.state.preproc.defringe is True assert w.state.preproc.use_threshold is True # a different loaded band is a different trace -> data-seeded defaults return - w.data.steps.buttons["entry"].click() + w.win.stage_buttons[Stage.DATA].click() lo, hi = sim.band.values() sim.band.set_values(lo + 5.0, hi - 5.0) sim._band = (lo + 5.0, hi - 5.0) diff --git a/tests/gui/test_workflow.py b/tests/gui/test_workflow.py index 9402130..43d9871 100644 --- a/tests/gui/test_workflow.py +++ b/tests/gui/test_workflow.py @@ -30,7 +30,13 @@ def test_workflow_stages_are_in_session_order_with_titles(): - assert [s.title for s in STAGES] == ["Data", "Prepare", "Retrieve", "Analyse"] + assert [s.title for s in STAGES] == [ + "Data", + "Check", + "Prepare", + "Retrieve", + "Analyse", + ] assert Stage.HOME not in STAGES @@ -40,6 +46,8 @@ def test_workflow_stages_are_in_session_order_with_titles(): (Stage.HOME, Progress(), False), (Stage.DATA, Progress(), True), # no session yet (Stage.DATA, FRESH, False), + (Stage.CHECK, FRESH, True), + (Stage.CHECK, LOADED, False), (Stage.PREPARE, FRESH, True), (Stage.PREPARE, LOADED, False), (Stage.RETRIEVE, LOADED, True), @@ -56,11 +64,16 @@ def test_workflow_raw_simulated_scan_skips_prepare_with_a_reason(): raw = Progress(entry="simulated", has_load_data=True, raw_direct=True) assert "Skip filtering & regrid" in blocked_reason(Stage.PREPARE, raw) assert next_stage(Stage.DATA, raw) is Stage.RETRIEVE + assert next_stage(Stage.CHECK, raw) is Stage.RETRIEVE @pytest.mark.parametrize( ("stage", "expected"), - [(Stage.DATA, Stage.PREPARE), (Stage.PREPARE, Stage.RETRIEVE)], + [ + (Stage.DATA, Stage.CHECK), + (Stage.CHECK, Stage.PREPARE), + (Stage.PREPARE, Stage.RETRIEVE), + ], ) def test_workflow_next_stage_follows_the_session(stage, expected): assert next_stage(stage, LOADED) is expected @@ -78,6 +91,8 @@ def test_workflow_next_stage_refuses_a_stage_without_continue(stage): (Stage.HOME, FRESH, "", Action("", "", enabled=False)), (Stage.DATA, FRESH, "", Action("continue", "Continue", enabled=False)), (Stage.DATA, LOADED, "", Action("continue", "Continue")), + (Stage.CHECK, FRESH, "", Action("continue", "Continue", enabled=False)), + (Stage.CHECK, LOADED, "", Action("continue", "Continue")), (Stage.PREPARE, LOADED, "", Action("continue", "Continue", enabled=False)), (Stage.PREPARE, PREPARED, "", Action("continue", "Continue")), (Stage.RETRIEVE, LOADED, "", Action("retrieve", "Retrieve", enabled=False)), From b445b0e70c61687601ab29525048981f8d7659d9 Mon Sep 17 00:00:00 2001 From: John Travers Date: Fri, 6 Nov 2026 10:08:31 +0000 Subject: [PATCH 26/41] Draw the screen views as publication figures croak.plotting.export draws any screen view into matplotlib axes with the same data, colours and line styles, and lays a page's views out at a journal column per plot. Maps are rasterized inside a vector file and text is kept as text. The role colours move into croak.plotting.screen, which the screen and the figures now share. Co-Authored-By: Claude Opus 5.5 --- croak/gui/plots.py | 14 +- croak/plotting/__init__.py | 3 + croak/plotting/export.py | 269 +++++++++++++++++++++++++++++++++ croak/plotting/screen.py | 54 +++++-- docs/howto/postprocessing.md | 5 +- docs/reference/api/plotting.md | 7 + tests/test_plotting_export.py | 160 ++++++++++++++++++++ 7 files changed, 484 insertions(+), 28 deletions(-) create mode 100644 croak/plotting/export.py create mode 100644 tests/test_plotting_export.py diff --git a/croak/gui/plots.py b/croak/gui/plots.py index eeb67c5..2ddd987 100644 --- a/croak/gui/plots.py +++ b/croak/gui/plots.py @@ -22,6 +22,7 @@ from PySide6.QtCore import QPointF, QRectF, Qt from PySide6.QtGui import QColor, QFont, QMouseEvent, QPainter +from ..plotting.screen import CURVE_COLOURS from .theme import FONT_FAMILY, PALETTE, PLOT_PX __all__ = [ @@ -38,17 +39,8 @@ pg.setConfigOptions(antialias=True, background="w", foreground=PALETTE.ink) -COLOURS = ( - "#1f6fb4", # blue - "#d2670b", # orange - "#2a9d6a", # green - "#8c5bb5", # purple - "#c2445c", # red - "#5a6675", # grey - "#b08a16", # ochre - "#2b8fb3", # cyan -) -"""The curves' colours, in order of use.""" +COLOURS = CURVE_COLOURS +"""The curves' colours, in order of use (shared with the exported figures).""" PEN_WIDTH = 1.7 """A curve's line width, in logical pixels.""" diff --git a/croak/plotting/__init__.py b/croak/plotting/__init__.py index 2925413..853cbad 100644 --- a/croak/plotting/__init__.py +++ b/croak/plotting/__init__.py @@ -41,6 +41,9 @@ ``screen`` What the GUI's interactive views show, as plain data (:mod:`croak.plotting.screen`). +``export`` + Those views drawn as publication figures, for the GUI's "Export … as a + figure" (:mod:`croak.plotting.export`). """ from .artists import ( diff --git a/croak/plotting/export.py b/croak/plotting/export.py new file mode 100644 index 0000000..44c9783 --- /dev/null +++ b/croak/plotting/export.py @@ -0,0 +1,269 @@ +"""Publication figures of what the GUI shows: screen views drawn by matplotlib. + +The window's plots are pyqtgraph, built from the Qt-free views of +:mod:`croak.plotting.screen`. :func:`draw_view` draws one of those views into a +matplotlib axes with the same data, colours and line styles, and +:func:`views_figure` lays a page's views out as one figure, so any plot or page +can be saved as a vector PDF or SVG (or a PNG). Images are rasterized inside a +vector file (``rasterized=True``): a trace map stays a few hundred kilobytes and +opens quickly, while its axes, labels and curves stay sharp. + +Examples +-------- +>>> import numpy as np +>>> from croak.plotting.screen import CurveView, LineView +>>> view = LineView( +... curves=(CurveView(np.arange(3.0), np.ones(3), "retrieved", "pulse"),), +... title="Temporal", +... xlabel="Time (fs)", +... ylabel="Intensity", +... ) +>>> fig = views_figure([[view]]) +>>> fig.axes[0].get_xlabel() +'Time (fs)' +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import matplotlib as mpl +from matplotlib.axes import Axes +from matplotlib.figure import Figure + +from .screen import ROLE_STYLE, LineView, MapView +from .traces import cmap_negwhite + +__all__ = [ + "EXPORT_RC", + "PANEL_SIZE", + "draw_line_view", + "draw_map_view", + "draw_view", + "save_figure", + "views_figure", +] + +PANEL_SIZE = (3.4, 2.6) +"""One panel's size in inches: a journal's single column is about 3.4 in wide.""" + +EXPORT_RC = { + "font.size": 8, + "axes.titlesize": 8, + "axes.labelsize": 8, + "xtick.labelsize": 7, + "ytick.labelsize": 7, + "legend.fontsize": 7, + "legend.frameon": False, + "xtick.direction": "in", + "ytick.direction": "in", + "xtick.top": True, + "ytick.right": True, + "lines.linewidth": 1.2, + "axes.linewidth": 0.6, + # Keep text as text in a vector file, so a journal's tools can edit it. + "pdf.fonttype": 42, + "ps.fonttype": 42, + "svg.fonttype": "none", +} +"""Matplotlib settings for a figure at print size: small type, inward ticks.""" + +EXPORT_DPI = 300 +"""Resolution of a PNG, and of the rasterized images inside a PDF or SVG.""" + +REFERENCE_WIDTH = 0.7 +"""The line width of a reference curve (the known pulse), points.""" + + +def draw_map_view( + ax: Axes, view: MapView, *, colours: str | None = None, white: bool = True +) -> None: + """Draw a map ``view`` into ``ax`` with a colour bar, as the screen shows it. + + Parameters + ---------- + ax : matplotlib.axes.Axes + Where to draw. + view : MapView + The image, its levels and labels. + colours : str, optional + The colour map (default: the view's own). + white : bool + Start the colour map at white, so an empty background reads as paper. + A signed view's negative values fade from red to white at zero. + """ + low, high = view.levels + negative = -low / (high - low) if view.signed and low < 0 < high else 0.0 + cmap = cmap_negwhite(colours or view.colours, white=white, negfrac=negative) + mesh = ax.pcolormesh( + view.x, + view.y, + view.values, + cmap=cmap, + vmin=low, + vmax=high, + shading="nearest", + rasterized=True, + ) + bar = ax.figure.colorbar(mesh, ax=ax, pad=0.02) + if view.unit: + bar.set_label(view.unit) + _label(ax, view.title, view.xlabel, view.ylabel) + if view.xrange is not None: + ax.set_xlim(*view.xrange) + if view.yrange is not None: + ax.set_ylim(*view.yrange) + + +def draw_line_view(ax: Axes, view: LineView) -> None: + """Draw a line ``view`` into ``ax``: curves, a right axis, bands and rules. + + Parameters + ---------- + ax : matplotlib.axes.Axes + Where to draw. + view : LineView + The curves and their decorations. + """ + right = None + if view.right_label is not None: + right = ax.twinx() + ax.tick_params(right=False) # the right axis has ticks of its own + if view.bars is not None: + colour = ROLE_STYLE[view.bars.role][0] + ax.stairs(view.bars.counts, view.bars.edges, fill=True, color=colour, alpha=0.5) + for span in view.spans: + colour = ROLE_STYLE[span.role][0] + shade = ax.axvspan if span.vertical else ax.axhspan + shade(span.low, span.high, color=colour, alpha=span.opacity, lw=0) + for band in view.bands: + colour = ROLE_STYLE[band.role][0] + ax.fill_between( + band.x, band.low, band.high, color=colour, alpha=band.opacity, lw=0 + ) + for curve in view.curves: + target = right if curve.right and right is not None else ax + colour, dashed, reference = ROLE_STYLE[curve.role] + if curve.points: + target.plot(curve.x, curve.y, ".", color=colour, ms=3, label=curve.name) + continue + target.plot( + curve.x, + curve.y, + color=colour, + ls="--" if dashed else "-", + lw=REFERENCE_WIDTH if reference else None, + zorder=1 if reference else 2, + label=curve.name, + ) + for y, role in view.levels: + ax.axhline(y, color=ROLE_STYLE[role][0], lw=0.8) + for x, role in view.marks: + ax.axvline(x, color=ROLE_STYLE[role][0], lw=0.8, ls="--") + if view.log_y: + ax.set_yscale("log") + _label(ax, view.title, view.xlabel, view.ylabel) + if view.notes: + ax.set_title(" ".join(text for text, _ in view.notes), loc="right") + if view.xrange is not None: + ax.set_xlim(*view.xrange) + if view.yrange is not None: + ax.set_ylim(*view.yrange) + if right is not None: + right.set_ylabel(view.right_label or "") + if view.right_range is not None: + right.set_ylim(*view.right_range) + _legend(ax, right) + + +def draw_view( + ax: Axes, + view: MapView | LineView, + *, + colours: str | None = None, + white: bool = True, +) -> None: + """Draw ``view`` into ``ax`` (a map or a line view; see the two drawers).""" + if isinstance(view, MapView): + draw_map_view(ax, view, colours=colours, white=white) + else: + draw_line_view(ax, view) + + +def views_figure( + views: Sequence[Sequence[MapView | LineView | None]], + *, + colours: Sequence[Sequence[str | None]] | None = None, + white: Sequence[Sequence[bool]] | None = None, +) -> Figure: + """Lay views out as a figure: rows of panels, as they sit on the page. + + Parameters + ---------- + views : sequence of sequence of view or None + The rows; ``None`` leaves a cell empty. + colours, white : sequence of sequence, optional + Per cell, a map's colour map and white-based scale (see + :func:`draw_map_view`); default: the view's own map, white-based. + + Returns + ------- + matplotlib.figure.Figure + At :data:`PANEL_SIZE` per panel, drawn with :data:`EXPORT_RC`. + + Raises + ------ + ValueError + If there is nothing to draw. + """ + rows = len(views) + columns = max((len(row) for row in views), default=0) + if not any(view is not None for row in views for view in row): + raise ValueError("a figure needs at least one view") + with mpl.rc_context(EXPORT_RC): + fig = Figure( + figsize=(PANEL_SIZE[0] * columns, PANEL_SIZE[1] * rows), + layout="constrained", + ) + axes = fig.subplots(rows, columns, squeeze=False) + for r, row in enumerate(views): + for c in range(columns): + view = row[c] if c < len(row) else None + ax = axes[r][c] + if view is None: + ax.set_axis_off() + continue + draw_view( + ax, + view, + colours=colours[r][c] if colours is not None else None, + white=white[r][c] if white is not None else True, + ) + return fig + + +def save_figure(fig: Figure, path: str) -> None: + """Save ``fig`` to ``path``; the extension picks PDF, SVG or PNG. + + Saved under :data:`EXPORT_RC`, which keeps text as editable text in a + vector file, at :data:`EXPORT_DPI`. + """ + with mpl.rc_context(EXPORT_RC): + fig.savefig(path, dpi=EXPORT_DPI) + + +def _label(ax: Axes, title: str, xlabel: str, ylabel: str) -> None: + ax.set_title(title, loc="left") + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + + +def _legend(ax: Axes, right: Axes | None) -> None: + """One legend for both axes, when more than one curve has a name.""" + handles, names = ax.get_legend_handles_labels() + if right is not None: + more = right.get_legend_handles_labels() + handles, names = handles + more[0], names + more[1] + named = [(h, n) for h, n in zip(handles, names, strict=True) if n] + if len(named) > 1: + ax.legend(*zip(*named, strict=True)) diff --git a/croak/plotting/screen.py b/croak/plotting/screen.py index ffb2214..90e6fd5 100644 --- a/croak/plotting/screen.py +++ b/croak/plotting/screen.py @@ -39,6 +39,8 @@ from .units import _TWOPI_PHZ __all__ = [ + "CURVE_COLOURS", + "ROLE_STYLE", "CurveView", "LineView", "MapView", @@ -62,23 +64,43 @@ "uniform_image", ] -# Roles a view gives its curves; the GUI maps each to a colour and a line style. -ROLES = ( - "retrieved", - "transform-limited", - "measured", - "truth", - "phase", - "fit", - "truth-phase", - "error", - "estimate", - "prior", - "predicted", - "corrected", - "faint", - "boundary", +CURVE_COLOURS = ( + "#1f6fb4", # blue + "#d2670b", # orange + "#2a9d6a", # green + "#8c5bb5", # purple + "#c2445c", # red + "#5a6675", # grey + "#b08a16", # ochre + "#2b8fb3", # cyan ) +"""The curves' colours, in order of use, on screen and in exported figures.""" + +_INK = "#1b2430" +_MUTED = "#5a6675" + +ROLE_STYLE: dict[str, tuple[str, bool, bool]] = { + "retrieved": (CURVE_COLOURS[0], False, False), + "transform-limited": (CURVE_COLOURS[1], False, False), + "measured": (CURVE_COLOURS[1], False, False), + "truth": (_INK, False, True), + "phase": (CURVE_COLOURS[2], False, False), + "fit": (CURVE_COLOURS[3], True, False), + "truth-phase": (_MUTED, True, True), + "error": (CURVE_COLOURS[0], False, False), + "estimate": (CURVE_COLOURS[4], False, False), + "prior": ("#9aa3ad", False, False), + "predicted": (CURVE_COLOURS[4], True, False), + "corrected": (CURVE_COLOURS[0], True, False), + "faint": ("#9cbfdd", False, False), + "boundary": (_MUTED, True, False), +} +"""Each role a view gives its curves: colour, dashed, and whether it is a thin +reference drawn beneath the rest (the known pulse). The screen and the exported +figures both draw from it.""" + +ROLES = tuple(ROLE_STYLE) +"""The roles a view may give its curves, bands, spans and rules.""" @dataclass(frozen=True) diff --git a/docs/howto/postprocessing.md b/docs/howto/postprocessing.md index ed720fa..8a3f5b6 100644 --- a/docs/howto/postprocessing.md +++ b/docs/howto/postprocessing.md @@ -280,7 +280,10 @@ own figures. ### Screen views (GUI support) The GUI does not draw these matplotlib panels on screen: it draws the same data -with interactive views, from the Qt-free {mod}`croak.plotting.screen`. See +with interactive views, from the Qt-free {mod}`croak.plotting.screen`, and +{mod}`croak.plotting.export` draws those views back into matplotlib for a +publication figure ({func}`~croak.plotting.export.views_figure`, +{func}`~croak.plotting.export.save_figure`). See [screen views in the developer’s guide](../developer_guide/gui_sessions.md#screen-views-gui-support). ## Windowing the retrieved field diff --git a/docs/reference/api/plotting.md b/docs/reference/api/plotting.md index 7337563..70f175b 100644 --- a/docs/reference/api/plotting.md +++ b/docs/reference/api/plotting.md @@ -8,3 +8,10 @@ and 4-panel composites. See [Post-processing and plotting](../../howto/postproce .. automodule:: croak.plotting :members: ``` + +## Figures of the GUI's views + +```{eval-rst} +.. automodule:: croak.plotting.export + :members: +``` diff --git a/tests/test_plotting_export.py b/tests/test_plotting_export.py new file mode 100644 index 0000000..97de5ea --- /dev/null +++ b/tests/test_plotting_export.py @@ -0,0 +1,160 @@ +"""Publication figures of the screen views (:mod:`croak.plotting.export`).""" + +from __future__ import annotations + +import numpy as np +import pytest + +from croak.plotting.export import ( + PANEL_SIZE, + draw_view, + save_figure, + views_figure, +) +from croak.plotting.screen import ( + BandView, + BarsView, + CurveView, + LineView, + MapView, + SpanView, +) + +X = np.linspace(-10.0, 10.0, 41) + + +def _line(**kwargs) -> LineView: + """A line view with every decoration a page can give one.""" + defaults = dict( + curves=( + CurveView(X, np.exp(-(X**2) / 20), "retrieved", "retrieved"), + CurveView(X, np.exp(-(X**2) / 18), "truth", "truth"), + CurveView(X[::5], np.full(9, 0.5), "estimate", "points", points=True), + CurveView(X, 0.01 * X**2, "phase", "phase", right=True), + ), + title="Temporal", + xlabel="Time (fs)", + ylabel="Intensity", + right_label="Phase (rad)", + xrange=(-8.0, 8.0), + yrange=(0.0, 1.1), + right_range=(-1.0, 2.0), + notes=(("FWHM 7 fs", False),), + marks=((0.0, "boundary"),), + levels=((0.5, "faint"),), + bands=(BandView(X, np.zeros_like(X), 0.2 + 0 * X, "estimate"),), + spans=(SpanView(-2.0, 2.0, "prior"), SpanView(0.1, 0.2, "fit", vertical=False)), + bars=BarsView(np.linspace(-5, 5, 6), np.arange(5.0), "estimate"), + ) + defaults.update(kwargs) + return LineView(**defaults) + + +def _map(signed: bool = False) -> MapView: + y = np.linspace(380.0, 420.0, 30) + values = np.exp(-(X[None] ** 2) / 30 - ((y[:, None] - 400) / 8) ** 2) + return MapView( + values=values - (0.05 if signed else 0.0), + x=X, + y=y, + levels=(-0.05 if signed else 0.0, 1.0), + title="Measured trace", + xlabel="Delay (fs)", + ylabel="Wavelength (nm)", + unit="a.u.", + signed=signed, + xrange=(-5.0, 5.0), + yrange=(390.0, 410.0), + ) + + +def test_export_line_view_keeps_its_curves_axes_and_ranges(): + fig = views_figure([[_line()]]) + ax, right = fig.axes + assert ax.get_xlabel() == "Time (fs)" and right.get_ylabel() == "Phase (rad)" + assert ax.get_xlim() == (-8.0, 8.0) and right.get_ylim() == (-1.0, 2.0) + assert ax.get_title(loc="left") == "Temporal" + assert ax.get_title(loc="right") == "FWHM 7 fs" + names = [text.get_text() for text in ax.get_legend().get_texts()] + assert names == ["retrieved", "truth", "points", "phase"] # both axes, one legend + truth = next(line for line in ax.lines if line.get_label() == "truth") + assert truth.get_linewidth() < 1.0 # a thin reference beneath the result + + +def test_export_line_view_with_one_curve_and_a_log_axis_has_no_legend(): + view = _line( + curves=(CurveView(X, 1 + X**2, "error"),), + right_label=None, + log_y=True, + yrange=None, + xrange=None, + notes=(), + bars=None, + ) + fig = views_figure([[view]]) + (ax,) = fig.axes + assert ax.get_yscale() == "log" and ax.get_legend() is None + + +@pytest.mark.parametrize("signed", [False, True]) +def test_export_map_view_draws_a_rasterized_image_with_its_scale(signed): + fig = views_figure([[_map(signed)]]) + ax = fig.axes[0] + (mesh,) = ax.collections + bar = mesh.colorbar.ax + assert mesh.get_rasterized() + assert mesh.get_clim() == pytest.approx((-0.05 if signed else 0.0, 1.0)) + # White at zero: an empty background reads as paper on either scale. + zero = mesh.cmap(mesh.norm(0.0)) + assert zero == pytest.approx((1.0, 1.0, 1.0, 1.0), abs=0.02) + assert bar.get_ylabel() == "a.u." + assert ax.get_ylim() == (390.0, 410.0) + + +def test_export_map_view_takes_a_panels_own_colour_map(): + fig = views_figure([[_map()]], colours=[["bwr"]], white=[[False]]) + mesh = fig.axes[0].collections[0] + assert mesh.cmap(0.0)[2] > 0.5 # bwr starts blue, not white + + +def test_export_figure_lays_rows_out_with_empty_cells(): + fig = views_figure([[_line(), _map()], [_map(), None]]) + assert tuple(fig.get_size_inches()) == pytest.approx( + (2 * PANEL_SIZE[0], 2 * PANEL_SIZE[1]) + ) + assert sum(not ax.axison for ax in fig.axes) == 1 # the empty cell + + +def test_export_figure_needs_a_view(): + with pytest.raises(ValueError, match="at least one view"): + views_figure([[None]]) + + +def test_export_draw_view_dispatches_on_the_kind(): + from matplotlib.figure import Figure + + ax = Figure().add_subplot() + draw_view(ax, _map()) + assert ax.collections + + +def test_export_saves_vector_files_with_text_kept_as_text(tmp_path): + fig = views_figure([[_line(), _map()]]) + pdf, svg, png = (tmp_path / f"page.{ext}" for ext in ("pdf", "svg", "png")) + for path in (pdf, svg, png): + save_figure(fig, str(path)) + assert b"/Image" in pdf.read_bytes() # the map, rasterized + assert b"/FontFile2" in pdf.read_bytes() # TrueType text, not outlines + content = svg.read_text() + assert " Date: Fri, 6 Nov 2026 10:08:31 +0000 Subject: [PATCH 27/41] Export any plot or page of the GUI as a figure A plot's right-click menu exports it as a figure, or saves or copies it as an image; Export this page as a figure (Ctrl+E) saves the page on screen, laid out as it is shown. Analyse's result pages keep croak's own page figures. ViewPanel moves to its own module. Co-Authored-By: Claude Opus 5.5 --- README.md | 7 +- changelog.d/gui-figure-export.added.md | 1 + croak/gui/pages.py | 14 ++ croak/gui/retrieval_views.py | 178 +-------------- croak/gui/stage.py | 19 ++ croak/gui/stage_dispersion.py | 2 +- croak/gui/stage_load.py | 2 +- croak/gui/stage_marginal_check.py | 2 +- croak/gui/stage_preprocess.py | 2 +- croak/gui/stage_simulated.py | 2 +- croak/gui/stage_synthetic.py | 2 +- croak/gui/stage_uncertainty.py | 2 +- croak/gui/theme.py | 3 + croak/gui/view_panel.py | 304 +++++++++++++++++++++++++ croak/gui/window.py | 28 ++- docs/developer_guide/architecture.md | 5 +- docs/developer_guide/gui_design.md | 29 ++- docs/developer_guide/gui_sessions.md | 22 +- docs/getting_started/gui.md | 14 +- docs/howto/gui.md | 101 +++++--- tests/gui/test_retrieval_views.py | 4 +- tests/gui/test_view_panel.py | 145 ++++++++++++ tests/gui/test_window.py | 11 + tests/gui/test_window_session.py | 15 ++ 24 files changed, 668 insertions(+), 246 deletions(-) create mode 100644 changelog.d/gui-figure-export.added.md create mode 100644 croak/gui/view_panel.py create mode 100644 tests/gui/test_view_panel.py diff --git a/README.md b/README.md index b08e8fc..7a1c7b4 100644 --- a/README.md +++ b/README.md @@ -235,9 +235,10 @@ from the underlying propagation physics. A PySide6 window drives the same library functions. A session starts on Home from a measured trace, a simulated scan or a synthetic pulse, and runs through -Data (load the trace and check its marginal) → Prepare → Retrieve → Analyse -(the retrieval's panels, dispersion tuning and uncertainty), with interactive -plots, threaded retrieval and a live preview: +Data → Check (the trace's marginal) → Prepare → Retrieve → Analyse (the +retrieval's panels, dispersion tuning and uncertainty), with interactive plots, +threaded retrieval, a live preview, and any plot or page exported as a vector +figure for publication: ```bash uv run croak-gui # or: uv run python -m croak.gui diff --git a/changelog.d/gui-figure-export.added.md b/changelog.d/gui-figure-export.added.md new file mode 100644 index 0000000..a3c16c0 --- /dev/null +++ b/changelog.d/gui-figure-export.added.md @@ -0,0 +1 @@ +Every plot and every page of the GUI can be saved as a figure for publication: a plot from its right-click menu (which can also save or copy it as an image), a page with "Export this page as a figure…" (Ctrl+E). Figures are drawn by matplotlib from the data on screen, at a single journal column per plot, as PDF, SVG or PNG; in a vector file the text stays editable and colour maps are rasterized. `croak.plotting.export` draws the same views from a script. diff --git a/croak/gui/pages.py b/croak/gui/pages.py index 1b9e53e..b47c691 100644 --- a/croak/gui/pages.py +++ b/croak/gui/pages.py @@ -13,6 +13,7 @@ from collections.abc import Callable, Mapping, Sequence from typing import cast +from matplotlib.figure import Figure from PySide6.QtCore import Qt from PySide6.QtGui import QKeySequence, QShortcut from PySide6.QtWidgets import ( @@ -29,6 +30,7 @@ ) from .base import Stage from .help import HelpDialog +from .home import ENTRY_TILES from .retrieval_views import ResultPage from .stage import PageBase from .stage_dispersion import StageDispersion @@ -41,6 +43,7 @@ from .stage_uncertainty import StageUncertainty from .state import WizardState from .theme import frame, set_flag +from .view_panel import file_name from .workflow import Stage as Place __all__ = ["AnalysePage", "DataPage", "RAIL_WIDTH", "default_pages"] @@ -48,6 +51,8 @@ RAIL_WIDTH = 170 """The width of Analyse's rail, logical px.""" +_ENTRY_TITLES = {entry: title for entry, title, _ in ENTRY_TILES} + class DataPage(PageBase): """Data: the entry's loader, which hands a trace on to the check. @@ -109,6 +114,9 @@ def act(self, kind: str) -> bool: def show_help(self) -> HelpDialog | None: return self.entry_page.show_help() + def figure_name(self) -> str: + return file_name(_ENTRY_TITLES[self.state.entry]) + # -- session replay ----------------------------------------------------- def restore(self) -> bool: """Reload a reopened session's trace with its saved options. @@ -258,6 +266,12 @@ def act(self, kind: str) -> bool: def show_help(self) -> HelpDialog | None: return self.pages[self.key].show_help() + def figure(self) -> Figure | None: + return self.pages[self.key].figure() + + def figure_name(self) -> str: + return self.pages[self.key].figure_name() + def default_pages(state: WizardState) -> Mapping[Place, PageBase]: """Build a session's Data, Check, Prepare, Retrieve and Analyse pages.""" diff --git a/croak/gui/retrieval_views.py b/croak/gui/retrieval_views.py index ebbad3a..6c1d8ab 100644 --- a/croak/gui/retrieval_views.py +++ b/croak/gui/retrieval_views.py @@ -11,14 +11,13 @@ from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass -from pathlib import Path from typing import cast import numpy as np +from matplotlib.figure import Figure from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, - QFileDialog, QGridLayout, QHBoxLayout, QMenu, @@ -36,21 +35,16 @@ ) from ..plotting import screen as sc from .plots import ( - COLOURS, - PEN_WIDTH, - Curve, LinePlot, - MapPlot, curve_reader, - map_reader, ) from .stage import PageBase from .state import WizardState -from .theme import PALETTE, label, set_flag +from .theme import label +from .view_panel import ViewPanel, draw_line_view __all__ = [ "OVERVIEW", - "ViewPanel", "PANELS", "Panel", "ResultPage", @@ -63,27 +57,6 @@ ) """The live overview: the traces above the pulse and its spectrum.""" -# Each curve role's colour, whether it is dashed, and whether it is a thin -# reference drawn beneath the rest (screen.ROLES), as in the matplotlib figures. -ROLE_STYLE: Mapping[str, tuple[str, bool, bool]] = { - "retrieved": (COLOURS[0], False, False), - "transform-limited": (COLOURS[1], False, False), - "measured": (COLOURS[1], False, False), - "truth": (PALETTE.ink, False, True), - "phase": (COLOURS[2], False, False), - "fit": (COLOURS[3], True, False), - "truth-phase": (PALETTE.muted, True, True), - "error": (COLOURS[0], False, False), - "predicted": (COLOURS[4], True, False), - "corrected": (COLOURS[0], True, False), - "faint": ("#9cbfdd", False, False), - "boundary": (PALETTE.muted, True, False), - "estimate": (COLOURS[4], False, False), - "prior": ("#9aa3ad", False, False), -} -REFERENCE_WIDTH = 0.9 -"""The width of a reference curve (the known pulse), logical px.""" - @dataclass(frozen=True) class Panel: @@ -130,128 +103,6 @@ def _retrieved_note(data: RetrievalPlotData) -> str: """The panels the screen can show, by the figure's keys.""" -def draw_line_view(plot: LinePlot, view: sc.LineView) -> None: - """Show ``view`` on ``plot``: curves, labels, ranges and marks.""" - curves = [] - for c in view.curves: - colour, dashed, under = ROLE_STYLE[c.role] - width = REFERENCE_WIDTH if under else PEN_WIDTH - curves.append( - Curve(c.x, c.y, c.name, colour, dashed, c.right, width, under, c.points) - ) - plot.set_labels(view.xlabel, view.ylabel) - if view.right_label is not None: - plot.set_right_label(view.right_label) - plot.set_curves(curves, log_y=view.log_y) - plot.clear_extras() - for span in view.spans: - colour = ROLE_STYLE[span.role][0] - plot.add_span(span.low, span.high, colour, span.opacity, vertical=span.vertical) - for band in view.bands: - plot.add_band( - band.x, band.low, band.high, ROLE_STYLE[band.role][0], band.opacity - ) - for y, role in view.levels: - plot.add_level(y, ROLE_STYLE[role][0]) - plot.fit() - if view.bars is not None: - bars = view.bars - plot.add_bars(bars.edges, bars.counts, ROLE_STYLE[bars.role][0]) - plot.set_view( - (float(bars.edges[0]), float(bars.edges[-1])), - (0.0, 1.05 * float(np.max(bars.counts)) or 1.0), - ) - if view.xrange is not None or view.yrange is not None: - (x0, x1), (y0, y1) = plot.box.viewRange() - xr = view.xrange or (x0, x1) - yr = view.yrange or (y0, y1) - if view.log_y and view.yrange is not None: # a log axis works in decades - yr = (float(np.log10(yr[0])), float(np.log10(yr[1]))) - plot.set_view(xr, yr) - if view.right_range is not None: - plot.set_right_range(*view.right_range) - plot.set_marks([(x, ROLE_STYLE[role][0]) for x, role in view.marks]) - - -def draw_map_view(plot: MapPlot, view: sc.MapView) -> None: - """Show ``view`` on ``plot``, then narrow it to the view's window.""" - plot.set_labels(view.xlabel, view.ylabel) - plot.show_map(view.values, view.x, view.y, view.levels) - (x0, x1), (y0, y1) = plot.box.viewRange() - plot.set_view(view.xrange or (x0, x1), view.yrange or (y0, y1)) - - -def _name(axis_label: str) -> str: - """Return an axis label's quantity for the readout: "Delay (fs)" → "Delay".""" - return axis_label.split(" (")[0] - - -class ViewPanel(QWidget): - """A titled plot that shows a screen view (:mod:`croak.plotting.screen`). - - Parameters - ---------- - kind : {"map", "line"} - The plot it holds. - colours : str - A map's colour map. - signed, white : bool - A map's colour scale (see :class:`~croak.gui.plots.MapPlot`). - right : bool - A line plot with a right-hand axis. - """ - - def __init__( - self, - kind: str, - *, - colours: str = "viridis", - signed: bool = False, - white: bool = True, - right: bool = False, - ) -> None: - super().__init__() - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(2) - head = QHBoxLayout() - self.title = label("", "plotTitle") - self.notes = label("", "pageNote") - self.notes.setAlignment(Qt.AlignmentFlag.AlignRight) - head.addWidget(self.title) - head.addStretch(1) - head.addWidget(self.notes) - layout.addLayout(head) - self.plot: MapPlot | LinePlot - if kind == "map": - self.plot = MapPlot(colours=colours, signed=signed, white=white) - else: - self.plot = LinePlot(right_label="" if right else None) - layout.addWidget(self.plot, stretch=1) - - def show_view(self, view: sc.MapView | sc.LineView, notes: str = "") -> None: - """Draw ``view``; ``notes`` (or a line view's own) go beside the title.""" - self.title.setText(view.title) - if isinstance(view, sc.MapView): - plot = cast(MapPlot, self.plot) - draw_map_view(plot, view) - plot.reader = map_reader(plot, _name(view.xlabel), _name(view.ylabel)) - self.notes.setText(notes) - set_flag(self.notes, "warn", False) - return - line = cast(LinePlot, self.plot) - draw_line_view(line, view) - line.reader = curve_reader(line, _name(view.xlabel)) - self.notes.setText(notes or " ".join(text for text, _ in view.notes)) - set_flag(self.notes, "warn", any(warn for _, warn in view.notes)) - - def say(self, title: str, text: str) -> None: - """Show ``text`` in place of a map (there is nothing to draw).""" - self.title.setText(title) - cast(MapPlot, self.plot).say(text) - self.notes.setText("") - - class PanelBox(ViewPanel): """One of the retrieval's panels (:data:`PANELS`).""" @@ -429,26 +280,17 @@ def pop_out(self) -> QDialog: dialog.show() return dialog - def export_figure(self) -> str: - """Save this page as a figure (PDF, PNG or SVG) drawn by matplotlib.""" + def figure(self) -> Figure | None: + """The page as croak's own figure of it (:func:`plot_retrieval_page`).""" data = self.plot_data() if data is None: - self.message.emit("Run a retrieval first: there is no figure to save.") - return "" - path, _ = QFileDialog.getSaveFileName( - self, - f"Save {self.title.lower()} as a figure", - f"{self.key}.pdf", - "PDF (*.pdf);;PNG (*.png);;SVG (*.svg)", - ) - if not path: - return "" + return None spec = ( PageSpec("overview", "Overview", self.mosaic) if self.key == "overview" else RETRIEVAL_PAGES[self.key] ) - figure = plot_retrieval_page(data, spec) - figure.savefig(path, dpi=300) - self.message.emit(f"Saved {Path(path).name}") - return path + return plot_retrieval_page(data, spec) + + def figure_name(self) -> str: + return self.key diff --git a/croak/gui/stage.py b/croak/gui/stage.py index 5e00268..e713d99 100644 --- a/croak/gui/stage.py +++ b/croak/gui/stage.py @@ -18,8 +18,11 @@ from .help import HelpDialog, HelpSections from .sheet import AdviceBox, DerivedGrid, Form, ProgressStrip, Sheet from .state import WizardState +from .view_panel import export_figure, file_name, page_figure if TYPE_CHECKING: + from matplotlib.figure import Figure + from ..plotting import RetrievalPlotData __all__ = ["PageBase", "StagePage"] @@ -91,6 +94,22 @@ def overview(self) -> RetrievalPlotData | None: """The retrieval this page shows, for a saved session's overview PDF.""" return None + def figure(self) -> Figure | None: + """The page as a publication figure; ``None`` while it shows nothing. + + By default every plot on the page, laid out as on the screen + (:func:`~croak.gui.view_panel.page_figure`). + """ + return page_figure(self) + + def figure_name(self) -> str: + """The file name a saved figure of the page is offered under.""" + return file_name(self.HELP_TITLE) + + def export_figure(self) -> str: + """Ask where to save :meth:`figure`; return the path, or ``""``.""" + return export_figure(self, self.figure(), self.figure_name(), self.message.emit) + def show_help(self) -> HelpDialog | None: """Open (or raise) the page's help dialog; ``None`` if it has no help.""" if not self.HELP: diff --git a/croak/gui/stage_dispersion.py b/croak/gui/stage_dispersion.py index b86a13f..67645a0 100644 --- a/croak/gui/stage_dispersion.py +++ b/croak/gui/stage_dispersion.py @@ -34,7 +34,7 @@ from .base import Stage from .controls import combo, group from .mirror_stack import MirrorStack -from .retrieval_views import ViewPanel +from .view_panel import ViewPanel from .widgets import SciSpinBox, ValueControl _GAS_NONE = "(none)" diff --git a/croak/gui/stage_load.py b/croak/gui/stage_load.py index b1513b7..e8bb667 100644 --- a/croak/gui/stage_load.py +++ b/croak/gui/stage_load.py @@ -28,8 +28,8 @@ from ..session.pipeline import assemble_load_data from .base import Stage from .controls import check, combo, group -from .retrieval_views import ViewPanel from .state import PreprocParams +from .view_panel import ViewPanel from .widgets import SciSpinBox _LAM_UNITS = ["nm", "µm", "m"] diff --git a/croak/gui/stage_marginal_check.py b/croak/gui/stage_marginal_check.py index 1959284..2f68384 100644 --- a/croak/gui/stage_marginal_check.py +++ b/croak/gui/stage_marginal_check.py @@ -47,7 +47,7 @@ from ..session.pipeline import assemble_load_data, assemble_simulated_load_data from .base import Stage from .controls import add_wide_row, check, combo, dspin, group -from .retrieval_views import ViewPanel +from .view_panel import ViewPanel from .widgets import RangeControl # Number of uniform angular-frequency samples used to resample the independent diff --git a/croak/gui/stage_preprocess.py b/croak/gui/stage_preprocess.py index 95408f0..0c23d76 100644 --- a/croak/gui/stage_preprocess.py +++ b/croak/gui/stage_preprocess.py @@ -41,7 +41,7 @@ group, spin, ) -from .retrieval_views import ViewPanel +from .view_panel import ViewPanel from .widgets import RangeControl, SciSpinBox from .worker import PreprocessWorker diff --git a/croak/gui/stage_simulated.py b/croak/gui/stage_simulated.py index 0675d5d..7f18f19 100644 --- a/croak/gui/stage_simulated.py +++ b/croak/gui/stage_simulated.py @@ -44,8 +44,8 @@ ) from .base import Stage from .controls import add_wide_row, check, combo, group -from .retrieval_views import ViewPanel from .stage_load import seed_preproc_defaults +from .view_panel import ViewPanel from .widgets import RangeControl # TG-FROG keeps the carrier (no doubling), so only the χ³ interactions apply. diff --git a/croak/gui/stage_synthetic.py b/croak/gui/stage_synthetic.py index 5cc310e..90419a0 100644 --- a/croak/gui/stage_synthetic.py +++ b/croak/gui/stage_synthetic.py @@ -38,8 +38,8 @@ group, spin, ) -from .retrieval_views import ViewPanel from .stage_load import seed_preproc_defaults +from .view_panel import ViewPanel from .widgets import RangeControl, ValueControl # dispersion slider ranges follow StageDispersion (fs² / fs³ / fs⁴) diff --git a/croak/gui/stage_uncertainty.py b/croak/gui/stage_uncertainty.py index bc54f32..58aae59 100644 --- a/croak/gui/stage_uncertainty.py +++ b/croak/gui/stage_uncertainty.py @@ -29,7 +29,7 @@ from ..uncertainty import UncertaintyResult, combine_uncertainties from .base import RunControls, Stage from .controls import check, combo, dspin, group, spin -from .retrieval_views import ViewPanel +from .view_panel import ViewPanel _METHODS = ["parametric", "delay", "frequency", "thickness", "covariance"] _NOISE_SOURCES = ["residual", "manual"] diff --git a/croak/gui/theme.py b/croak/gui/theme.py index 3c39d05..256da8d 100644 --- a/croak/gui/theme.py +++ b/croak/gui/theme.py @@ -37,6 +37,9 @@ _log = logging.getLogger(__name__) ASSETS = Path(__file__).parent / "assets" + +NOTICE_MS = 20_000 +"""How long a passing notice stays in the status bar, ms.""" """Where the fonts and the drawn assets live.""" FONT_FAMILY = "IBM Plex Sans" diff --git a/croak/gui/view_panel.py b/croak/gui/view_panel.py new file mode 100644 index 0000000..fe87667 --- /dev/null +++ b/croak/gui/view_panel.py @@ -0,0 +1,304 @@ +"""A titled plot of one screen view, which can leave the window as a figure. + +:class:`ViewPanel` shows a view of :mod:`croak.plotting.screen` on a pyqtgraph +plot (:func:`draw_line_view`, :func:`draw_map_view`). A right-click offers the +plot as a publication figure, drawn by matplotlib from the same view +(:mod:`croak.plotting.export`), or as an image of the screen. +:func:`page_figure` lays out every plot a page shows as one figure, and +:func:`export_figure` asks where to save one. +""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +from typing import cast + +import numpy as np +from matplotlib.figure import Figure +from PySide6.QtCore import QPoint, Qt +from PySide6.QtWidgets import ( + QApplication, + QFileDialog, + QHBoxLayout, + QMainWindow, + QMenu, + QVBoxLayout, + QWidget, +) + +from ..plotting import screen as sc +from ..plotting.export import save_figure, views_figure +from ..plotting.screen import ROLE_STYLE +from .plots import PEN_WIDTH, Curve, LinePlot, MapPlot, curve_reader, map_reader +from .theme import NOTICE_MS, label, set_flag + +__all__ = [ + "ViewPanel", + "draw_line_view", + "draw_map_view", + "export_figure", + "file_name", + "page_figure", +] + +FIGURE_FILTERS = "PDF (*.pdf);;SVG (*.svg);;PNG (*.png)" +"""The figure formats offered: two vector ones first, then a bitmap.""" + +REFERENCE_WIDTH = 0.9 +"""The width of a reference curve (the known pulse), logical px.""" + + +def draw_line_view(plot: LinePlot, view: sc.LineView) -> None: + """Show ``view`` on ``plot``: curves, labels, ranges and marks.""" + curves = [] + for c in view.curves: + colour, dashed, under = ROLE_STYLE[c.role] + width = REFERENCE_WIDTH if under else PEN_WIDTH + curves.append( + Curve(c.x, c.y, c.name, colour, dashed, c.right, width, under, c.points) + ) + plot.set_labels(view.xlabel, view.ylabel) + if view.right_label is not None: + plot.set_right_label(view.right_label) + plot.set_curves(curves, log_y=view.log_y) + plot.clear_extras() + for span in view.spans: + colour = ROLE_STYLE[span.role][0] + plot.add_span(span.low, span.high, colour, span.opacity, vertical=span.vertical) + for band in view.bands: + plot.add_band( + band.x, band.low, band.high, ROLE_STYLE[band.role][0], band.opacity + ) + for y, role in view.levels: + plot.add_level(y, ROLE_STYLE[role][0]) + plot.fit() + if view.bars is not None: + bars = view.bars + plot.add_bars(bars.edges, bars.counts, ROLE_STYLE[bars.role][0]) + plot.set_view( + (float(bars.edges[0]), float(bars.edges[-1])), + (0.0, 1.05 * float(np.max(bars.counts)) or 1.0), + ) + if view.xrange is not None or view.yrange is not None: + (x0, x1), (y0, y1) = plot.box.viewRange() + xr = view.xrange or (x0, x1) + yr = view.yrange or (y0, y1) + if view.log_y and view.yrange is not None: # a log axis works in decades + yr = (float(np.log10(yr[0])), float(np.log10(yr[1]))) + plot.set_view(xr, yr) + if view.right_range is not None: + plot.set_right_range(*view.right_range) + plot.set_marks([(x, ROLE_STYLE[role][0]) for x, role in view.marks]) + + +def draw_map_view(plot: MapPlot, view: sc.MapView) -> None: + """Show ``view`` on ``plot``, then narrow it to the view's window.""" + plot.set_labels(view.xlabel, view.ylabel) + plot.show_map(view.values, view.x, view.y, view.levels) + (x0, x1), (y0, y1) = plot.box.viewRange() + plot.set_view(view.xrange or (x0, x1), view.yrange or (y0, y1)) + + +def _name(axis_label: str) -> str: + """Return an axis label's quantity for the readout: "Delay (fs)" → "Delay".""" + return axis_label.split(" (")[0] + + +class ViewPanel(QWidget): + """A titled plot that shows a screen view (:mod:`croak.plotting.screen`). + + Parameters + ---------- + kind : {"map", "line"} + The plot it holds. + colours : str + A map's colour map. + signed, white : bool + A map's colour scale (see :class:`~croak.gui.plots.MapPlot`). + right : bool + A line plot with a right-hand axis. + + Attributes + ---------- + view : MapView or LineView or None + What the plot shows now; ``None`` before anything is drawn. + """ + + def __init__( + self, + kind: str, + *, + colours: str = "viridis", + signed: bool = False, + white: bool = True, + right: bool = False, + ) -> None: + super().__init__() + self.colours = colours + self.white = white + self.view: sc.MapView | sc.LineView | None = None + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + head = QHBoxLayout() + self.title = label("", "plotTitle") + self.notes = label("", "pageNote") + self.notes.setAlignment(Qt.AlignmentFlag.AlignRight) + head.addWidget(self.title) + head.addStretch(1) + head.addWidget(self.notes) + layout.addLayout(head) + self.plot: MapPlot | LinePlot + if kind == "map": + self.plot = MapPlot(colours=colours, signed=signed, white=white) + else: + self.plot = LinePlot(right_label="" if right else None) + layout.addWidget(self.plot, stretch=1) + self.plot.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.plot.customContextMenuRequested.connect(self.show_menu) + + def show_view(self, view: sc.MapView | sc.LineView, notes: str = "") -> None: + """Draw ``view``; ``notes`` (or a line view's own) go beside the title.""" + self.view = view + self.title.setText(view.title) + if isinstance(view, sc.MapView): + plot = cast(MapPlot, self.plot) + draw_map_view(plot, view) + plot.reader = map_reader(plot, _name(view.xlabel), _name(view.ylabel)) + self.notes.setText(notes) + set_flag(self.notes, "warn", False) + return + line = cast(LinePlot, self.plot) + draw_line_view(line, view) + line.reader = curve_reader(line, _name(view.xlabel)) + self.notes.setText(notes or " ".join(text for text, _ in view.notes)) + set_flag(self.notes, "warn", any(warn for _, warn in view.notes)) + + def say(self, title: str, text: str) -> None: + """Show ``text`` in place of a map (there is nothing to draw).""" + self.view = None + self.title.setText(title) + cast(MapPlot, self.plot).say(text) + self.notes.setText("") + + # -- leaving the window --------------------------------------------------- + def figure(self) -> Figure | None: + """This plot as a publication figure; ``None`` while it shows nothing.""" + if self.view is None: + return None + return views_figure( + [[self.view]], colours=[[self.colours]], white=[[self.white]] + ) + + def show_menu(self, where: QPoint) -> QMenu: + """Offer the plot as a figure or an image, at ``where`` on the plot.""" + menu = QMenu(self) + export = menu.addAction("Export this plot as a figure…", self.export_figure) + export.setEnabled(self.view is not None) + menu.addSeparator() + menu.addAction("Save as an image…", self.save_image) + menu.addAction("Copy as an image", self.copy_image) + menu.popup(self.plot.mapToGlobal(where)) + return menu + + def export_figure(self) -> str: + """Ask where to save this plot as a figure; return the path, or ``""``.""" + name = file_name(self.title.text()) + return export_figure(self, self.figure(), name, self._say) + + def _say(self, text: str) -> None: + """Show ``text`` in the status bar of the window, if it has one.""" + window = self.window() + if isinstance(window, QMainWindow): + window.statusBar().showMessage(text, NOTICE_MS) + + def save_image(self) -> str: + """Ask where to save the plot as drawn on screen (PNG); return the path.""" + path, _ = QFileDialog.getSaveFileName( + self, + "Save the plot as an image", + f"{file_name(self.title.text())}.png", + "PNG (*.png)", + ) + if path: + self.plot.grab().save(path) + return path + + def copy_image(self) -> None: + """Put the plot, as drawn on screen, on the clipboard.""" + QApplication.clipboard().setPixmap(self.plot.grab()) + + +def file_name(title: str) -> str: + """Return a file name for a title, or ``"figure"`` for none. + + Examples + -------- + >>> file_name("Measured trace (log)") + 'measured_trace_log' + """ + words = "".join(c if c.isalnum() else " " for c in title.lower()).split() + return "_".join(words) or "figure" + + +def export_figure( + parent: QWidget, figure: Figure | None, name: str, say: Callable[[str], None] +) -> str: + """Ask where to save ``figure`` (PDF, SVG or PNG) and save it there. + + Parameters + ---------- + parent : QWidget + The dialog's parent. + figure : matplotlib.figure.Figure or None + The figure; ``None`` when there is nothing to save, which is said. + name : str + The suggested file name, without its extension. + say : callable + Shows a short notice (what was saved, or why nothing was). + + Returns + ------- + str + The path saved to, or ``""`` if nothing was saved. + """ + if figure is None: + say("There is nothing to save as a figure yet.") + return "" + path, _ = QFileDialog.getSaveFileName( + parent, "Save as a figure", f"{name}.pdf", FIGURE_FILTERS + ) + if not path: + return "" + save_figure(figure, path) + say(f"Saved {Path(path).name}") + return path + + +def page_figure(page: QWidget) -> Figure | None: + """Every plot ``page`` shows, as one figure laid out as on the screen. + + The plots are taken in rows by their top edge, left to right; a plot that + is hidden or shows nothing is left out. ``None`` if no plot shows anything. + """ + panels = [ + panel + for panel in page.findChildren(ViewPanel) + if panel.view is not None and panel.isVisibleTo(page) + ] + if not panels: + return None + rows: dict[int, list[ViewPanel]] = {} + for panel in panels: + corner = panel.mapTo(page, QPoint(0, 0)) + rows.setdefault(corner.y(), []).append(panel) + ordered = [ + sorted(row, key=lambda panel: panel.mapTo(page, QPoint(0, 0)).x()) + for _, row in sorted(rows.items()) + ] + return views_figure( + [[cast(sc.MapView | sc.LineView, p.view) for p in row] for row in ordered], + colours=[[p.colours for p in row] for row in ordered], + white=[[p.white for p in row] for row in ordered], + ) diff --git a/croak/gui/window.py b/croak/gui/window.py index 93c637a..5d189f4 100644 --- a/croak/gui/window.py +++ b/croak/gui/window.py @@ -44,7 +44,16 @@ from .home import ENTRY_TILES, Home from .stage import PageBase from .state import WizardState -from .theme import ASSETS, button, frame, label, load_fonts, set_flag, stylesheet +from .theme import ( + ASSETS, + NOTICE_MS, + button, + frame, + label, + load_fonts, + set_flag, + stylesheet, +) from .workflow import ( STAGES, Action, @@ -69,8 +78,6 @@ """The width of the settings drawer, logical px.""" TOP_BAR_HEIGHT = 52 """The height of the top bar, logical px.""" -NOTICE_MS = 20_000 -"""How long a passing notice stays in the status bar, ms.""" _ENTRY_TITLES = {entry: title for entry, title, _ in ENTRY_TILES} @@ -238,6 +245,11 @@ def _make_menus(self) -> None: "Show the settings (options.toml)", self.show_options ), "python": self._action("Show as a Python script", self.show_python), + "figure": self._action( + "Export this page as a figure…", + self.export_figure, + QKeySequence("Ctrl+E"), + ), "help": self._action( "Help for this stage", self.show_help, QKeySequence(keys.HelpContents) ), @@ -246,7 +258,7 @@ def _make_menus(self) -> None: # On macOS, Qt moves it to the application menu, where About belongs. self.actions_["about"].setMenuRole(QAction.MenuRole.AboutRole) overflow = QMenu(self) - for key in ("save", "open"): + for key in ("save", "open", "figure"): overflow.addAction(self.actions_[key]) overflow.addSeparator() for key in ("options", "python"): @@ -257,7 +269,7 @@ def _make_menus(self) -> None: self.overflow.setMenu(overflow) bar = self.menuBar() file_menu = bar.addMenu("&File") - for key in ("home", "open", "save"): + for key in ("home", "open", "save", "figure"): file_menu.addAction(self.actions_[key]) view_menu = bar.addMenu("&View") for key in ("options", "python"): @@ -327,6 +339,7 @@ def refresh(self) -> None: self.actions_["save"].setEnabled(progress.has_result) for key in ("options", "python"): self.actions_[key].setEnabled(bool(progress.entry)) + self.actions_["figure"].setEnabled(self.stage in self.pages) def go(self, stage: Stage) -> bool: """Show ``stage`` if it can be opened; say why not otherwise.""" @@ -359,6 +372,11 @@ def go_home(self) -> None: """Show Home (the session stays open until a new one starts).""" self.show_home() + def export_figure(self) -> str: + """Save the page on screen as a publication figure; return the path.""" + page = self.pages.get(self.stage) + return page.export_figure() if page is not None else "" + def show_about(self) -> None: """Show About, keeping any open session.""" self.go(Stage.ABOUT) diff --git a/docs/developer_guide/architecture.md b/docs/developer_guide/architecture.md index a3ff89c..883d6f1 100644 --- a/docs/developer_guide/architecture.md +++ b/docs/developer_guide/architecture.md @@ -25,10 +25,11 @@ stage onto the library, replays a saved session and writes a session's files; the GUI, `croak replay` and `croak script` all go through it. The screen layer, `croak.plotting.screen`, turns the data behind a figure into the arrays an interactive view draws, using the same helpers as the matplotlib panels, so -the screen and an exported figure agree. +the screen and an exported figure agree; `croak.plotting.export` draws any +screen view as a matplotlib figure, for the GUI's figure export. On top of them, `croak.gui.window.MainWindow` takes a session from Home through -Data, Prepare, Retrieve and Analyse. The route between the stages is a set of +Data, Check, Prepare, Retrieve and Analyse. The route between the stages is a set of pure rules (`croak.gui.workflow`); each stage is a page that binds its controls to the session's parameters and hands long computations to worker threads; and every on-screen plot is a pyqtgraph view of screen data. Numerics belong in the diff --git a/docs/developer_guide/gui_design.md b/docs/developer_guide/gui_design.md index eb1fea9..224ead1 100644 --- a/docs/developer_guide/gui_design.md +++ b/docs/developer_guide/gui_design.md @@ -95,10 +95,13 @@ and `error`, which `theme.set_flag` sets and re-applies. - **Target**: 1366×768 logical pixels and up, opened maximised. - **Top bar**: 52 px high, with a hairline beneath. Brand and context at the - left, the stage buttons in the middle, the primary action and ⋯ at the right. -- **Sheet**: the form (at least 360 px) and the plots share the width 2 : 3. - Below a window width of 1000 px the plots move under the form. The form - scrolls on its own; its help line and ready line stay at its foot. + left; Home, a hairline divider and the stage buttons in the middle; the + primary action and ⋯ at the right. +- **Sheet**: the form is a fixed 340 px column (`sheet.FORM_WIDTH`) and the plots + take the rest of the width. Below a window width of 1000 px the plots move + under the form, which then spans the window. The form scrolls on its own; its + help line and ready line stay at its foot, and its combo boxes shrink to the + column rather than widen it. - **Sections** fold in place. Settings most sessions leave alone start folded and remember their state (a `settings_key`), as Retrieve's advanced sections do. @@ -109,6 +112,10 @@ and `error`, which `theme.set_flag` sets and re-applies. - **Strips** beneath the plots that have a natural height (Retrieve's numeric read-out) fold away and hand their height back to the plots. - **Analyse** has a 170 px rail of pages on the left. +- **Export**: every plot has a right-click menu (figure, image, copy), and every + page a figure (`PageBase.figure`), saved by Ctrl+E. Figures are drawn from the + screen views by `croak.plotting.export`, with maps rasterized in a vector + file. - **Drawer**: 440 px on the right, for text the user may copy (the settings, the script). @@ -116,9 +123,10 @@ and `error`, which `theme.set_flag` sets and re-applies. | Place | Page | Primary action | Plots | |---|---|---|---| -| Home | tiles, Open a session…, recent sessions | none | none | +| Home | tiles, Open a session…, About croak, recent sessions | none | none | +| About | version, links (manual, GitHub, PyPI, issues), authors, copyright | none | none | | Data | Load the trace / Load the scan / Generate the trace | Continue | the loaded trace; for a scan its maps and marginals; for a synthetic pulse the pulse, spectrum, spectrogram and trace | -| Data | Check the marginal | Continue | the trace and the marginal comparison | +| Check | — | Continue | the trace and the marginal comparison | | Prepare | — | Continue | measured, filtered and regridded traces in dB and linear (2×3) | | Retrieve | — | Retrieve / Stop / Retrieve again | the live overview, or convergence before the first preview; the numeric read-out | | Analyse | Overview, Traces, Pulse, Diagnostics | Save | the retrieval's panels, two by two | @@ -132,10 +140,11 @@ These are known gaps, kept out of the redesign to keep it reviewable: - **A dark theme.** The style sheet is built from one `Palette`, so a dark one would be a second instance, but the plots' white background and colour maps would need choosing too. -- **The unused sheet parts.** `sheet.AdviceBox` (an amber box saying what went - wrong and what to do), `DerivedGrid` (numbers derived from the settings) and - `ProgressStrip` (state, bar and figures over the plots while work runs) are - built but no stage uses them yet; a run's progress is on the ready line. +- **The unused sheet parts.** `sheet.DerivedGrid` (numbers derived from the + settings) and `ProgressStrip` (state, bar and figures over the plots while + work runs) are built but no stage uses them yet; a run's progress is on the + ready line. `AdviceBox` so far carries one piece of advice: Prepare's, when + the λ grid clips the trace. - **The stage pages' interface.** The stage modules still build through `base.Stage`, which keeps the calls they were written against for the old wizard; moving them onto `StagePage` directly is a later clean-up. diff --git a/docs/developer_guide/gui_sessions.md b/docs/developer_guide/gui_sessions.md index 1e63c79..0f0359e 100644 --- a/docs/developer_guide/gui_sessions.md +++ b/docs/developer_guide/gui_sessions.md @@ -46,13 +46,15 @@ tested without a window. |---|---| | `window` | `MainWindow`: top bar, stage stack, drawer, menus, and the session actions (new, open, replay, retarget, save) | | `workflow` | the route as pure rules: `Stage`, `Progress`, `blocked_reason`, `next_stage`, `primary_action`, `remember_recent` | -| `home` | Home: the entry tiles, Open a session… and the recent sessions | -| `pages` | `default_pages` (the page factory), `DataPage` (loader and marginal check as two steps) and `AnalysePage` (the rail) | +| `home` | Home: the entry tiles, Open a session…, About croak and the recent sessions | +| `about` | About: the version, links, authors and copyright, from the package metadata | +| `pages` | `default_pages` (the page factory), `DataPage` (the entry's loader, and a reopened session's restore) and `AnalysePage` (the rail) | | `stage` | `PageBase`, the hooks the window reads, and `StagePage`, a page that is a sheet | | `base` | `Stage`, the interface the stage modules are written against (`controls`, `set_plot_area`, `set_status`, `advance`), and `RunControls` | | `sheet` | the sheet's parts: `Form`, `Section`, `ReadyLine`, `Segments`, `AdviceBox`, `DerivedGrid`, `ProgressStrip` and `Sheet` itself | -| `stage_load`, `stage_simulated`, `stage_synthetic` | the three loaders (Data's first step) | +| `stage_load`, `stage_simulated`, `stage_synthetic` | the three loaders (Data) | | `stage_marginal_check`, `stage_preprocess`, `stage_retrieve`, `stage_dispersion`, `stage_uncertainty` | the other stage pages | +| `view_panel` | `ViewPanel`, a titled plot of one screen view with its right-click menu, and the figure export of a plot or a page (`page_figure`, `export_figure`) | | `retrieval_views` | the retrieval's panels on screen (`RetrievalPanels`) and Analyse's result pages (`ResultPage`, with Export and Pop out) | | `plots` | the pyqtgraph views: `Plot`, `MapPlot`, `LinePlot`, the colour maps and the hover readers | | `theme` | the palette, type scale, style sheet and the role helpers (`label`, `button`, `frame`, `set_flag`) | @@ -116,9 +118,13 @@ shows only the positive part, leaving negative samples to the signed linear maps pages and the exported figures share the panel names and layouts ({data}`~croak.plotting.RETRIEVAL_PAGES`). A live retrieval calls `RetrievalPanels.set_data` on the plots it already has, which replaces their data -without rebuilding the widgets. Exported figures — **Export ▾ → Figure…**, the -pages' **Save…** buttons, a saved session's `retrieval.pdf` — go through the -matplotlib `draw_*` and `plot_*` functions. +without rebuilding the widgets. The figures of Analyse's result pages, the +pages' **Save…** buttons and a saved session's `retrieval.pdf` go through the +matplotlib `draw_*` and `plot_*` functions. Every other figure — a plot's +**Export this plot as a figure…**, or **Export this page as a figure…** on a page +without a figure of its own — is drawn from the screen views themselves by +`croak.plotting.export`, with the same role colours (`screen.ROLE_STYLE`), so a +new view can be exported without writing its figure. The screen functions are Qt-free and tested in `tests/test_plotting_screen.py`; a new view starts there. @@ -151,8 +157,8 @@ environment), driving the real widgets through `qtbot`. They count towards the Many stage tests were written for the old six-stage wizard. `tests/gui/gui_session.py` keeps them working: its `Driver` builds a -`MainWindow`, maps the old stage numbers onto the window (1 the loader and 2 the -marginal check on Data, 3 Prepare, 4 Retrieve, 5 Dispersion and 6 Uncertainty on +`MainWindow`, maps the old stage numbers onto the window (1 the loader on Data, +2 Check, 3 Prepare, 4 Retrieve, 5 Dispersion and 6 Uncertainty on Analyse) and hands back the stage pages, which keep their behaviour. `goto` bypasses the route rules, as the old stage setter did, so a test can look at a page before its input exists. Tests of navigation itself use the window and the diff --git a/docs/getting_started/gui.md b/docs/getting_started/gui.md index 9f9aadf..98a772a 100644 --- a/docs/getting_started/gui.md +++ b/docs/getting_started/gui.md @@ -69,11 +69,11 @@ un-maximised, it returns to the size you last left it. It opens on **Home**: Pick a tile on Home — **Measured trace** (an HDF5 or `.npz` file, with an optional independent spectrum from HDF5, `.npz` or CSV and calibration curves from CSV), **Simulated scan** or **Synthetic trace** (a pulse you specify, to try -croak without data). The session then runs through four stages, shown as +croak without data). The session then runs through five stages, shown as buttons in the top bar: -- **Data** — load (or generate) the trace, then check its frequency marginal - against the independent spectrum; +- **Data** — load (or generate) the trace; +- **Check** — compare its frequency marginal with the independent spectrum; - **Prepare** — filtering, background removal, windowing and regridding; - **Retrieve** — choose a solver and run it, with a live preview; - **Analyse** — the retrieval's pages of panels, then **Dispersion** (tuning with @@ -82,10 +82,12 @@ buttons in the top bar: The button at the right of the top bar does the next thing — **Continue**, **Retrieve**, **Save** — and a stage you cannot open yet is greyed out, with the -reason in its tooltip. Settings are on the left of each page and plots on the -right; hover over a control for help with that setting, and over a plot to read +reason in its tooltip; **Home** at the start of the row goes back to Home. +Settings are in a narrow column on the left of each page and the plots fill the +rest; hover over a control for help with that setting, and over a plot to read its values. Scroll to zoom, drag to pan, and double-click to return to the -starting view. +starting view. Right-click a plot, or press Ctrl+E (⌘E) for the whole page, to +save it as a figure for publication: a vector PDF or SVG drawn by matplotlib. ```{image} ../_static/gui_retrieve.png :alt: Retrieve after a run — the solver settings on the left, the measured and retrieved traces above the retrieved pulse and spectrum on the right, and the numeric read-out beneath. diff --git a/docs/howto/gui.md b/docs/howto/gui.md index 5b1ee90..3ec8005 100644 --- a/docs/howto/gui.md +++ b/docs/howto/gui.md @@ -6,8 +6,8 @@ same calculations as the Python interface. If this is your first visit, follow [installing the GUI](../getting_started/gui.md), then try a synthetic trace whose pulse is known. -A session runs through four stages — **Data → Prepare → Retrieve → Analyse** — -which you reach from **Home**. The top bar always shows where you are, which +A session runs through five stages — **Data → Check → Prepare → Retrieve → +Analyse** — which you reach from **Home**. The top bar always shows where you are, which stages you can open and the one action that moves the session on. ## Launching @@ -42,16 +42,18 @@ so they look the same as the figures a script makes. The window is laid out for displays of **1366×768 logical pixels and up** (a 1920×1080 monitor at Windows' 125 % scaling is 1536×864 logical pixels). Each -stage puts its settings on the left and its plots on the right; when the window -is narrower than 1000 pixels the plots move below the settings instead. The +stage puts its settings in a narrow column on the left (340 pixels) and gives the +rest of the window to its plots; when the window is narrower than 1000 pixels +the plots move below the settings instead. The settings are grouped in sections that fold, and Retrieve's advanced sections start folded, so a laptop screen shows the settings you use most without scrolling. The numeric read-out beneath Retrieve's plots folds away with a click on its header, and returns the height to the plots. -Plot text stays at the same size whatever the window size. Figures written by -**Export ▾ → Figure…** or by a **Save** are drawn afresh at their own design -size, so they do not depend on the window either. +Plot text stays at the same size whatever the window size. Exported figures +(see [Figures for publication](#figures-for-publication)) and the files a +**Save** writes are drawn afresh at their own design size, so they do not depend +on the window either. ### Desktop integration @@ -80,7 +82,9 @@ data: dispersion and noise, and test a retrieval against the known answer. Beneath the tiles, **Open a session…** reopens a saved session from its -`options.toml` (see [Sessions](#sessions)). The **Recent sessions** list on the +`options.toml` (see [Sessions](#sessions)), and **About croak** opens the About +page: the version, links to this manual, the source on GitHub, the package on +PyPI and the issue tracker, the authors, and the copyright and licences. The **Recent sessions** list on the right holds the last six sessions you saved or opened, named by their folder, with the folder's location and the time the session was last saved; a session whose file has gone is left out. Click one to reopen it. @@ -93,20 +97,23 @@ or **Cancel**; **Save session…** in the ⋯ menu keeps it. The top bar carries, from left to right: -- the **croak** brand, which returns to Home. The session stays open: its stage - buttons are still there, so you can go back to it; -- a context label: what the session is (*Measured trace*, *Simulated scan* or - *Synthetic trace*) and, once it has been saved or opened, its folder's name; -- the four stage buttons **Data · Prepare · Retrieve · Analyse**. A stage that - cannot be opened yet is greyed out, and its tooltip says why; +- the **croak** brand and a context label: what the session is (*Measured + trace*, *Simulated scan* or *Synthetic trace*) and, once it has been saved or + opened, its folder's name; +- **Home**, which returns to Home (so does the brand). The session stays open: + its stage buttons are still there, so you can go back to it; +- the five stage buttons **Data · Check · Prepare · Retrieve · Analyse**. A + stage that cannot be opened yet is greyed out, and its tooltip says why; - the **primary action**, the one button that moves the session on; -- the **⋯** menu: **Save session…**, **Open a session…**, **Show the settings - (options.toml)**, **Show as a Python script** and **Help for this stage**. - -The menu bar repeats these under **File** (Home, Open a session… and Save -session…, with the platform's Open and Save shortcuts), **View** (the settings -and the script) and **Help** (Help for this stage, F1 on Windows and Linux, ⌘? -on macOS). Passing notices, such as "Saved …" or why a stage cannot open, +- the **⋯** menu: **Save session…**, **Open a session…**, **Export this page as + a figure…**, **Show the settings (options.toml)**, **Show as a Python + script**, **Help for this stage** and **About croak**. + +The menu bar repeats these under **File** (Home, Open a session…, Save +session… and Export this page as a figure…, with the platform's Open and Save +shortcuts and Ctrl+E, ⌘E on macOS), **View** (the settings and the script) and +**Help** (Help for this stage, F1 on Windows and Linux, ⌘? on macOS, and About +croak, which macOS moves to the application menu). Passing notices, such as "Saved …" or why a stage cannot open, appear in a status bar at the foot of the window, which hides again when there is nothing to say. @@ -117,12 +124,15 @@ Which stages can be opened depends only on what the session holds: | Stage | Opens once | |---|---| | Data | a session has started | +| Check | a trace has been loaded | | Prepare | a trace has been loaded | | Retrieve | the trace has been prepared | | Analyse | a retrieval has run | A simulated scan loaded with **Skip filtering & regrid** is retrieved on its own -grid, so it goes from Data straight to Retrieve and Prepare stays disabled. +grid, so it goes from Data straight to Retrieve and Prepare stays disabled +(Check stays open, to look at the scan, and its Continue also goes to +Retrieve). Changing a setting upstream clears what was derived from it — a new trace clears the prepared trace and the retrieval — so the GUI never shows a pulse retrieved from superseded settings, and the later stages close again until they are @@ -132,7 +142,8 @@ The primary action follows the stage: | Where | Primary action | |---|---| -| Data | **Continue**: from the loader to the marginal check, then to Prepare | +| Data | **Continue**: load or generate the trace and go on to Check | +| Check | **Continue** to Prepare | | Prepare | **Continue** to Retrieve | | Retrieve | **Retrieve**, **Stop** while it runs, then **Retrieve again** | | Analyse | **Save** (the session); on the Uncertainty page **Estimate**, or **Stop** while it runs | @@ -171,18 +182,39 @@ shows the positive part of the signal; negative samples, left by background subtraction, show on the linear maps, which run from white to viridis for positive values and from white to red for negative ones. +**Right-click** a plot for its own menu: **Export this plot as a figure…**, +**Save as an image…** (a PNG of the plot as it is on screen) and **Copy as an +image**. + The pages on Analyse also have **Export ▾**: **Figure…** saves that page as a -PDF, PNG or SVG drawn by matplotlib, and **Pop out** opens it in a window of its -own, which follows later results. +figure (as **Export this page as a figure…** does), and **Pop out** opens it in +a window of its own, which follows later results. + +### Figures for publication + +Any page, or any single plot, can be saved as a figure drawn by matplotlib: +**Export this page as a figure…** (⋯ menu, File menu or Ctrl+E) for the page on +screen, or **Export this plot as a figure…** from a plot's right-click menu. +Choose **PDF** or **SVG** for a vector figure, or **PNG**. In a vector figure the +axes, curves and text stay sharp and the text stays editable text; the colour +maps are embedded as images at 300 dpi (rasterized), so a trace map stays small +and opens quickly. A PNG is written at 300 dpi. + +The figure shows the same data as the screen, with the same colours, line styles +and labels, sized for print: each plot is 3.4 × 2.6 inches (a journal's single +column) in 8-point type, and a page keeps its plots' arrangement. Analyse's +Overview, Traces, Pulse and Diagnostics pages are saved as croak's own figure of +that page ({func}`~croak.plotting.plot_retrieval_page`), as a script makes it. +From a script, {mod}`croak.plotting.export` draws the same screen views. ## The stages ### Data -Data has two steps, shown as segments at the top of the page: the loader for the -session's kind of data, then **Check the marginal**. The check step opens once a -trace is loaded. **Continue** on the loader moves to the check, and Continue on -the check moves to Prepare. +Data holds the loader for the session's kind of data. **Continue** loads (or +generates) the trace and moves on to [Check](#check). Check opens as soon as a +trace is loaded, so a measured trace loaded with **Load & Preview** can be +checked from the top bar straight away. #### Load the trace (measured) @@ -217,7 +249,7 @@ signal so an aggressive efficiency tilt cannot wash the image out. Load a numerically simulated `scansave` scan and preview it live. A **thickness** dropdown selects which saved propagation depth to retrieve from a multi-thickness scan (disabled for older single-thickness files). Continue loads -the scan and moves to the marginal check; with **Skip filtering & regrid (raw → +the scan and moves to Check; with **Skip filtering & regrid (raw → retrieval)** ticked it builds the as-simulated trace on its native grid and goes straight to Retrieve. See [Loading numerically simulated traces](loading_simulated.md). @@ -235,11 +267,11 @@ delay and wavelength samplings each expose an interlocked **count** and downstream (range / step / # points); by default it **mirrors the FROG-trace λ window**, scaled to the fundamental (×2 for SHG). With it present, the marginal-check overlay and the SHG marginal correction work on synthetic traces -too. Continue generates the trace and moves to the marginal check. The known +too. Continue generates the trace and moves to Check. The known pulse (its temporal intensity envelope `It`) is carried through, shown beneath the retrieved pulse, and saved with the result. -#### Check the marginal +### Check Compare the trace's frequency marginal with the marginal **predicted** from the independent spectrum ({mod}`croak.marginal_checks`): exact for SHG (the spectral @@ -260,9 +292,8 @@ predicted SHG autoconvolution ({func}`~croak.marginal_checks.apply_marginal_correction`), is enabled only for an SHG trace with an independent spectrum, and previews the snap onto the prediction live (the correction itself is applied in Prepare). All three kinds -of session pass through this step, except a raw simulated scan; see -[Marginal consistency checks](marginal_checks.md). The trace and the marginal -comparison share a draggable divider, so either plot can be given the height. +of session pass through this stage, except a raw simulated scan; see +[Marginal consistency checks](marginal_checks.md). ### Prepare diff --git a/tests/gui/test_retrieval_views.py b/tests/gui/test_retrieval_views.py index cce9716..eed4d3e 100644 --- a/tests/gui/test_retrieval_views.py +++ b/tests/gui/test_retrieval_views.py @@ -170,13 +170,13 @@ def test_result_page_exports_its_figure(qtbot, data, tmp_path, monkeypatch): nothing.message.connect(messages.append) qtbot.addWidget(nothing) assert nothing.export_figure() == "" - assert "Run a retrieval first" in messages[-1] + assert messages[-1] == "There is nothing to save as a figure yet." def test_view_panel_draws_bars_bands_spans_points_and_levels(qtbot): from test_plotting_screen import _estimate - from croak.gui.retrieval_views import ViewPanel + from croak.gui.view_panel import ViewPanel from croak.plotting.screen import uncertainty_views panels = [ViewPanel("line") for _ in range(3)] diff --git a/tests/gui/test_view_panel.py b/tests/gui/test_view_panel.py new file mode 100644 index 0000000..21bd6d4 --- /dev/null +++ b/tests/gui/test_view_panel.py @@ -0,0 +1,145 @@ +"""A plot's way out of the window: figures and images (:mod:`croak.gui.view_panel`).""" + +from __future__ import annotations + +import numpy as np +import pytest + +pytest.importorskip("PySide6") + +from PySide6.QtCore import QPoint +from PySide6.QtWidgets import ( + QApplication, + QFileDialog, + QGridLayout, + QMainWindow, + QWidget, +) + +from croak.gui.view_panel import ViewPanel, export_figure, page_figure +from croak.plotting.screen import CurveView, LineView, MapView + +X = np.linspace(0.0, 1.0, 8) +LINE = LineView( + curves=(CurveView(X, X, "retrieved"),), + title="Temporal", + xlabel="Time (fs)", + ylabel="Intensity", +) +MAP = MapView( + values=np.outer(X, X), + x=X, + y=X, + levels=(0.0, 1.0), + title="Measured trace", + xlabel="Delay (fs)", + ylabel="Wavelength (nm)", +) + + +def _save_to(monkeypatch, path): + monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: (path, "")) + + +@pytest.fixture +def window(qtbot): + """A main window, so a panel's notices have a status bar to go to.""" + window = QMainWindow() + qtbot.addWidget(window) + window.resize(800, 600) + return window + + +def test_view_panel_menu_offers_its_figure_once_it_shows_a_view(qtbot): + panel = ViewPanel("map", colours="magma", white=False) + qtbot.addWidget(panel) + assert panel.figure() is None + menu = panel.show_menu(QPoint(5, 5)) + actions = {a.text(): a for a in menu.actions() if a.text()} + assert not actions["Export this plot as a figure…"].isEnabled() + assert set(actions) == { + "Export this plot as a figure…", + "Save as an image…", + "Copy as an image", + } + panel.show_view(MAP) + figure = panel.figure() + assert figure is not None + mesh = figure.axes[0].collections[0] + assert mesh.cmap(0.0)[:3] != (1.0, 1.0, 1.0) # the panel's own map, not white + panel.say("Residuals", "No residual for this result.") + assert panel.figure() is None + + +def test_view_panel_right_click_opens_its_menu(qtbot, monkeypatch): + panel = ViewPanel("line") + qtbot.addWidget(panel) + opened = [] + monkeypatch.setattr(panel, "show_menu", opened.append) + panel.plot.customContextMenuRequested.emit(QPoint(3, 4)) + assert opened == [QPoint(3, 4)] + + +def test_view_panel_exports_its_figure_and_says_so(window, monkeypatch, tmp_path): + panel = ViewPanel("line") + window.setCentralWidget(panel) + panel.show_view(LINE) + _save_to(monkeypatch, "") + assert panel.export_figure() == "" # cancelled + target = tmp_path / "temporal.svg" + _save_to(monkeypatch, str(target)) + assert panel.export_figure() == str(target) + assert "

    4V|7r}VLfss*oNv$FJ_&a6K` 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%0A$Zqv7qb6F1o1`&|uLuZ3 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 RJ-&KJA_9{9xxAY0{|iE0_>TYp literal 196262 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