Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
934c768
Move the GUI from PyQt6 to PySide6
jtravs Nov 5, 2026
bb0dfae
Add the redesigned GUI's foundations
jtravs Nov 5, 2026
998804d
Document croak.plotting's re-exported constants again
jtravs Nov 5, 2026
c3e25c2
Split croak.plotting into a package by domain
jtravs Nov 5, 2026
8609f46
Add the workflow window, Home and the stage-page base
jtravs Nov 5, 2026
43e87be
Merge branch 'refs/heads/claude/cranky-solomon-42d541' into claude/cr…
jtravs Nov 5, 2026
45f6bdc
Restyle the stages on the new sheet layout
jtravs Nov 5, 2026
1743c9a
Make the workflow window the GUI and retire the wizard
jtravs Nov 5, 2026
d7e29e8
Draw the retrieval on pyqtgraph and give Analyse its result pages
jtravs Nov 5, 2026
68948c7
Draw Load and the marginal check on pyqtgraph
jtravs Nov 5, 2026
9361437
Draw the synthetic and simulated previews on pyqtgraph
jtravs Nov 5, 2026
3ca6761
Draw the preprocessing preview on pyqtgraph
jtravs Nov 5, 2026
6a0d419
Draw Dispersion and Uncertainty on pyqtgraph
jtravs Nov 5, 2026
fe24e6b
Remove the matplotlib canvas and live-preview helpers
jtravs Nov 5, 2026
e974727
Screenshot the workflow window for the docs; fix a stray Home hint
jtravs Nov 5, 2026
be7835b
State the live preview's real rate in its help
jtravs Nov 5, 2026
c777754
Describe the redesigned GUI in its release note
jtravs Nov 5, 2026
a253e8d
Offer Find a setting on Retrieve's form
jtravs Nov 5, 2026
4d8a6ce
Step through Analyse's rail with the arrow keys
jtravs Nov 5, 2026
d32062c
Bring the GUI's own texts in line with the window
jtravs Nov 5, 2026
4578b1c
Document the workflow window
jtravs Nov 5, 2026
021f5ce
Draw the read-out fold's chevrons from SVG
jtravs Nov 6, 2026
f59d1c7
Say on Prepare when the λ grid clips the trace
jtravs Nov 6, 2026
c509d90
Preview the synthetic trace as Continue hands it on
jtravs Nov 6, 2026
8db0f27
Give the plots the width: a fixed, narrow form column
jtravs Nov 6, 2026
6ee6415
Make Check a stage; add a Home button and an About page
jtravs Nov 6, 2026
b445b0e
Draw the screen views as publication figures
jtravs Nov 6, 2026
c4ac72c
Export any plot or page of the GUI as a figure
jtravs Nov 6, 2026
af58daf
Record the review's fixes in the release notes
jtravs Nov 6, 2026
268be37
Fit every form to its column
jtravs Nov 6, 2026
9690731
Retake the GUI screenshots for the new layout
jtravs Nov 6, 2026
e973d40
Leave running and saving to the top bar
jtravs Nov 6, 2026
3b62fee
Move on to Analyse from Retrieve; show the run's numbers large
jtravs Nov 6, 2026
911179a
Show every plot on one chosen spectral axis and log range
jtravs Nov 6, 2026
5cbd024
Draw Home's tiles and make Open a session one of them
jtravs Nov 6, 2026
b7e07a1
Document the display settings, Retrieve's numbers and Home's tiles
jtravs Nov 6, 2026
5140b0f
Delete the progress strip and the page numbers grid nobody used
jtravs Nov 6, 2026
6c05952
Save everything from Save session; drop the pages' own Save buttons
jtravs Nov 6, 2026
ce46f38
Show the grid's numbers over Prepare's maps
jtravs Nov 6, 2026
5e5b313
Drop Retrieve's ready line; give sliders the room their boxes took
jtravs Nov 6, 2026
5bd7adc
Save the current retrieval's dispersion and uncertainty, not stale ones
jtravs Nov 6, 2026
d2bf19a
Narrow the shown estimate's type before plotting it
jtravs Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -56,7 +56,7 @@ methods and the validation are described in two papers in preparation (see
the [capabilities overview](https://croak.readthedocs.io/en/latest/reference/features.html)
lists everything the package does.

![The wizard after a retrieval: measured and retrieved traces, residuals, the retrieved pulse and spectrum, and a numeric read-out.](docs/_static/gui_retrieve.png)
![The GUI after a retrieval: measured and retrieved traces, the retrieved pulse and spectrum, and the run's numbers beside its convergence.](docs/_static/gui_retrieve.png)

## Install

Expand Down Expand Up @@ -96,7 +96,7 @@ uv run croak-gui
<details>
<summary><b>Platform notes</b>: read this if the install fails</summary>

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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -233,9 +233,12 @@ from the underlying propagation physics.

## GUI

A PyQt6 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 → 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
Expand Down
1 change: 1 addition & 0 deletions changelog.d/gui-display-settings.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Two GUI display settings, at the foot of the window, hold for every plot and are remembered between launches: **Spectral axis** draws every spectral plot, maps and curves, against wavelength or frequency, with the density Jacobian, or keeps each on the axis it was computed on; **Log range** sets how far below its peak every logarithmic map reaches (5–120 dB). A map's colour bar can also be dragged to change that map's scale alone. Retrieve's own Spectrum axis control and Check's marginal axis control give way to them. `croak.plotting.display` does the conversion from a script.
1 change: 1 addition & 0 deletions changelog.d/gui-figure-export.added.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions changelog.d/gui-matplotlib-live-preview.removed.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions changelog.d/gui-one-save.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The GUI has one way to save: **Save session…** now also writes what Analyse holds, the dispersed pulse (`result_dispersion.h5`, `dispersion.pdf`) when a dispersion is set and the uncertainty figure (`uncertainty.pdf`) when an estimate is on screen, and the Dispersion and Uncertainty pages lose their own Save… buttons. Prepare shows five numbers over its maps: the grid's size, time step, time window, wavelength span and the share of the trace its edge taper removes.
1 change: 1 addition & 0 deletions changelog.d/gui-prepare-taper-advice.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The GUI now says when the retrieval grid's λ range cuts off signal that is still in the trace: Prepare shows the share of the trace the edge taper removes and how to widen the range. The warning used to reach only the terminal.
1 change: 1 addition & 0 deletions changelog.d/gui-preview-help.fixed.md
Original file line number Diff line number Diff line change
@@ -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).
1 change: 1 addition & 0 deletions changelog.d/gui-pyside6.changed.md
Original file line number Diff line number Diff line change
@@ -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`.
1 change: 1 addition & 0 deletions changelog.d/gui-retrieve-notices.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Retrieve no longer has a ready line under its form: the numbers beneath its plots say how a run goes, its other messages are notices at the foot of the window, and a failed retrieval shows in the advice box over the plots. A slider's value box is now as wide as the numbers it takes, so the slider has the rest of the row, and a range has its two ends either side of one slider (TOD on the synthetic pulse page, among others, had a box wider than its slider).
1 change: 1 addition & 0 deletions changelog.d/gui-retrieve-strip.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
On Retrieve, once a retrieval is done, the top bar's primary action is **Analyse**, with **Retrieve again** beside it. Beneath the plots the run's numbers (iteration, error, elapsed time, the retrieved peak wavelength and FWHM, the forward-model extras and the truth errors) are shown large, beside the convergence curve, which follows the whole run; the pulse-parameter table is gone. Home's tiles carry drawings, and **Open a session** is a tile of its own.
1 change: 1 addition & 0 deletions changelog.d/gui-signed-map-noise.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A signed linear map in the GUI no longer paints its whole zero background red when its only negative values are rounding noise (Prepare's filtered and regridded traces), and the synthetic generator's spectrum preview is now a wavelength density, as its spectrogram already was. Log axes are labelled at decades only, so a short convergence plot no longer piles its labels up.
1 change: 1 addition & 0 deletions changelog.d/gui-synthetic-preview.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The synthetic generator's trace preview shows the trace that Continue hands on, sampled onto the λ window; it used to show a featureless stripe. A flat phase no longer fills its axis with rounding noise: phase axes span at least ±1 rad, in the GUI and in croak's figures.
1 change: 1 addition & 0 deletions changelog.d/gui-workflow-window.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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), Check (its marginal), Prepare, Retrieve and Analyse (Overview, Traces, Pulse, Diagnostics, Dispersion and Uncertainty pages), with a Home button and one primary action in the top bar, an About page (version, links to the manual, GitHub and PyPI, authors and copyright), 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.
42 changes: 21 additions & 21 deletions croak/gui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""PyQt6 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, Check, 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
Expand All @@ -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.

Expand All @@ -37,36 +37,36 @@ 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 "
"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

Expand Down
6 changes: 3 additions & 3 deletions croak/gui/__main__.py
Original file line number Diff line number Diff line change
@@ -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())
105 changes: 105 additions & 0 deletions croak/gui/about.py
Original file line number Diff line number Diff line change
@@ -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'<a href="{url}" style="{style}">{text}</a>', "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))
1 change: 1 addition & 0 deletions croak/gui/assets/chevron-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions croak/gui/assets/chevron.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added croak/gui/assets/fonts/IBMPlexSans-Medium.ttf
Binary file not shown.
Binary file added croak/gui/assets/fonts/IBMPlexSans-Regular.ttf
Binary file not shown.
Binary file added croak/gui/assets/fonts/IBMPlexSans-SemiBold.ttf
Binary file not shown.
Loading
Loading