Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions probeflow/gui/convert/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from probeflow.gui.typography import ui_font
from probeflow.gui.typography import mono_font, ui_font
from PySide6.QtCore import Qt
from PySide6.QtGui import QCursor, QFont
from PySide6.QtWidgets import (
Expand Down Expand Up @@ -95,7 +95,7 @@ def __init__(self, t: dict, cfg: dict, parent=None):

self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setFont(QFont("Courier", 11))
self.log_text.setFont(mono_font(11))
lay.addWidget(self.log_text, 1)

if cfg.get("input_dir"):
Expand Down
14 changes: 7 additions & 7 deletions probeflow/gui/dialogs/fft_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from probeflow.gui.typography import ui_font
from probeflow.gui.typography import mono_font, ui_font
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QAction, QActionGroup, QFont
from PySide6.QtWidgets import (
Expand Down Expand Up @@ -456,7 +456,7 @@ def _build_fft_column(self) -> QVBoxLayout:
cursor_title = QLabel("Cursor")
cursor_title.setFont(ui_font(9, weight=QFont.Bold))
self._cursor_readout_lbl = QLabel("Move over the FFT")
self._cursor_readout_lbl.setFont(QFont("Courier", 8))
self._cursor_readout_lbl.setFont(mono_font(8))
self._cursor_readout_lbl.setWordWrap(True)
side_lay.addWidget(cursor_title)
side_lay.addWidget(self._cursor_readout_lbl)
Expand Down Expand Up @@ -507,7 +507,7 @@ def _build_fft_column(self) -> QVBoxLayout:

# Measurement summary — always visible at the top
self._grid_measure_lbl = QLabel("No grid — click Draw Grid to start")
self._grid_measure_lbl.setFont(QFont("Courier", 9))
self._grid_measure_lbl.setFont(mono_font(9))
self._grid_measure_lbl.setWordWrap(True)
self._grid_measure_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse)
grid_outer_lay.addWidget(self._grid_measure_lbl)
Expand Down Expand Up @@ -648,7 +648,7 @@ def _build_fft_column(self) -> QVBoxLayout:
ref_grid.setColumnStretch(1, 1)
ref_grid.setColumnStretch(3, 1)
self._bragg_radius_lbl = QLabel("Shells: —")
self._bragg_radius_lbl.setFont(QFont("Courier", 8))
self._bragg_radius_lbl.setFont(mono_font(8))
self._bragg_radius_lbl.setWordWrap(True)
self._bragg_radius_lbl.setMaximumHeight(34)
self._bragg_radius_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse)
Expand All @@ -665,7 +665,7 @@ def _build_fft_column(self) -> QVBoxLayout:
self._fft_measured_lbl = QLabel(
"Draw a grid and select a known structure to see the comparison."
)
self._fft_measured_lbl.setFont(QFont("Courier", 8))
self._fft_measured_lbl.setFont(mono_font(8))
self._fft_measured_lbl.setWordWrap(True)
self._fft_measured_lbl.setMinimumHeight(32)
self._fft_measured_lbl.setMaximumHeight(54)
Expand Down Expand Up @@ -728,7 +728,7 @@ def _build_fft_column(self) -> QVBoxLayout:
self._fft_fill_combo.addItems(["NaN", "Background", "Zero"])

self._fft_correction_lbl = QLabel("Align a reciprocal grid to compute correction factors.")
self._fft_correction_lbl.setFont(QFont("Courier", 8))
self._fft_correction_lbl.setFont(mono_font(8))
self._fft_correction_lbl.setWordWrap(True)
self._fft_correction_lbl.setMinimumHeight(34)
self._fft_correction_lbl.setMaximumHeight(60)
Expand Down Expand Up @@ -880,7 +880,7 @@ def _build(self):
info_lay.setContentsMargins(8, 6, 8, 4)
info_lay.setSpacing(2)
self._info_lbl = QLabel("")
self._info_lbl.setFont(QFont("Courier", 9))
self._info_lbl.setFont(mono_font(9))
self._info_lbl.setAlignment(Qt.AlignTop | Qt.AlignLeft)
info_lay.addWidget(self._info_lbl)
left_col.addWidget(info_frame)
Expand Down
6 changes: 5 additions & 1 deletion probeflow/gui/dialogs/fft_viewer_reconstruct_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ def _build_reconstruct_tab(self) -> QWidget:
"Remove selected: suppress the selected Fourier features and rebuild "
"the corrected image. Keep selected: rebuild only the selected "
"periodic component (everything else removed)."))
self._recon_mode_combo.currentIndexChanged.connect(self._update_reconstruct_status)
# Drop the combo's int index — _update_reconstruct_status treats its
# first positional arg as a reconstruction result, so passing the index
# straight through made it call res.imag_residual_norm on an int.
self._recon_mode_combo.currentIndexChanged.connect(
lambda _idx: self._update_reconstruct_status())
self._recon_mode_combo.setMaximumWidth(140)
mode_row.addWidget(self._recon_mode_combo)
mode_row.addStretch(1)
Expand Down
8 changes: 3 additions & 5 deletions probeflow/gui/dialogs/image_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QDialog,
QFormLayout,
Expand All @@ -19,6 +17,8 @@
QWidget,
)

from probeflow.gui.typography import mono_font

if TYPE_CHECKING:
from probeflow.core.metadata import ScanMetadata

Expand Down Expand Up @@ -168,9 +168,7 @@ def _row(field: str, text: str) -> None:
hist_lay.setContentsMargins(4, 4, 4, 4)
hist_text = QPlainTextEdit()
hist_text.setReadOnly(True)
mono_font = QFont("Menlo" if Path("/System/Library/Fonts/Menlo.ttc").exists() else "Courier")
mono_font.setPointSize(9)
hist_text.setFont(mono_font)
hist_text.setFont(mono_font(9))
hist_text.setPlainText(processing_history_text or "(No processing history)")
hist_lay.addWidget(hist_text)
tabs.addTab(hist_widget, "Processing History")
Expand Down
5 changes: 2 additions & 3 deletions probeflow/gui/dialogs/periodic_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from probeflow.gui.typography import ui_font
from probeflow.gui.typography import mono_font, ui_font
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QFrame, QHBoxLayout, QLabel, QPushButton, QSlider, QVBoxLayout, QWidget,
)
Expand Down Expand Up @@ -97,7 +96,7 @@ def _build(self):
info_lay.setContentsMargins(8, 6, 8, 4)
info_lay.setSpacing(2)
self._info_lbl = QLabel("")
self._info_lbl.setFont(QFont("Courier", 9))
self._info_lbl.setFont(mono_font(9))
self._info_lbl.setAlignment(Qt.AlignTop | Qt.AlignLeft)
info_lay.addWidget(self._info_lbl)
info_lay.addStretch(1)
Expand Down
8 changes: 4 additions & 4 deletions probeflow/gui/lattice_grid/real_space_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import numpy as np

from probeflow.gui.typography import ui_font
from probeflow.gui.typography import mono_font, ui_font
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
Expand Down Expand Up @@ -280,7 +280,7 @@ def _spin_row(
meas_lay = QVBoxLayout(meas_grp)
meas_lay.setContentsMargins(6, 6, 6, 4)
self._meas_lbl = QLabel("")
self._meas_lbl.setFont(QFont("Courier", 8))
self._meas_lbl.setFont(mono_font(8))
self._meas_lbl.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self._meas_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse)
self._meas_lbl.setWordWrap(True)
Expand Down Expand Up @@ -393,7 +393,7 @@ def _spin_row(
meas_dist_lay = QVBoxLayout(meas_dist_grp)
meas_dist_lay.setContentsMargins(6, 6, 6, 4)
self._measured_lbl = QLabel("(tune grid above)")
self._measured_lbl.setFont(QFont("Courier", 8))
self._measured_lbl.setFont(mono_font(8))
self._measured_lbl.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self._measured_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse)
meas_dist_lay.addWidget(self._measured_lbl)
Expand All @@ -405,7 +405,7 @@ def _spin_row(
corr_lay = QVBoxLayout(corr_grp)
corr_lay.setContentsMargins(6, 6, 6, 4)
self._correction_lbl = QLabel("(enter ideal lattice above)")
self._correction_lbl.setFont(QFont("Courier", 8))
self._correction_lbl.setFont(mono_font(8))
self._correction_lbl.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self._correction_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse)
self._correction_lbl.setWordWrap(True)
Expand Down
41 changes: 40 additions & 1 deletion probeflow/gui/typography.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
}

_UI_FAMILY: str | None = None
_MONO_FAMILY: str | None = None


def ui_family() -> str:
Expand All @@ -47,6 +48,27 @@ def ui_family() -> str:
return _UI_FAMILY


def mono_family() -> str:
"""Return the platform fixed-width font family (Menlo on macOS), cached.

Asking Qt for the real system monospace family avoids the costly font-alias
lookup — and the "missing font family 'Courier'" warning — that an explicit
``QFont("Courier", ...)`` triggers on macOS/Windows, where "Courier" is only
an alias rather than an installed family.
"""
global _MONO_FAMILY
if _MONO_FAMILY is None:
family = ""
try:
from PySide6.QtGui import QFontDatabase

family = QFontDatabase.systemFont(QFontDatabase.FixedFont).family()
except Exception:
family = ""
_MONO_FAMILY = family or "Monospace"
return _MONO_FAMILY


def scaled(pt: int) -> int:
"""Map a legacy point size onto the comfortable scale."""
return _SIZE_MAP.get(int(pt), int(pt))
Expand All @@ -64,4 +86,21 @@ def ui_font(pt: int, *, bold: bool = False, weight=None) -> "QFont":
return f


__all__ = ["ui_family", "scaled", "ui_font"]
def mono_font(pt: int, *, bold: bool = False, weight=None) -> "QFont":
"""Build a fixed-width font in the real platform monospace family.

The point size is preserved verbatim (not run through ``scaled``): these are
compact numeric readouts whose layout depends on the existing size — the fix
here is only the font *family*, to silence the "Courier" alias warning.
"""
from PySide6.QtGui import QFont

f = QFont(mono_family(), pt)
if weight is not None:
f.setWeight(weight)
elif bold:
f.setBold(True)
return f


__all__ = ["ui_family", "mono_family", "scaled", "ui_font", "mono_font"]
10 changes: 9 additions & 1 deletion probeflow/processing/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,10 +475,18 @@ def apply_processing_state(
)
elif step.op == "stm_background":
fit_mask = _resolve_mask_roi_param(p, "fit", a.shape, roi_set)
fit_region = str(p.get("fit_region", "whole_image"))
if fit_region == "active_roi" and fit_mask is None:
# The ROI mask could not be resolved (no roi_set, missing ROI,
# or non-area ROI) — _resolve_mask_roi_param already warned and
# returned None. Degrade to a whole-image fit instead of letting
# the kernel raise (active_roi mandates a mask), so thumbnail /
# preview renders without a roi_set don't crash.
fit_region = "whole_image"
a = _proc.apply_stm_background(
a,
_proc.STMBackgroundParams(
fit_region=str(p.get("fit_region", "whole_image")),
fit_region=fit_region,
line_statistic=str(p.get("line_statistic", "median")),
model=str(p.get("model", "linear")),
linear_x_first=bool(p.get("linear_x_first", False)),
Expand Down
13 changes: 13 additions & 0 deletions tests/test_fft_phase_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ def _set_view(dlg, text):
dlg._fft_view_combo.setCurrentIndex(dlg._fft_view_combo.findText(text))


class TestReconstructStatus:
def test_changing_reconstruct_mode_does_not_crash(self, qapp):
# currentIndexChanged emits the int index; _update_reconstruct_status
# treats its first positional arg as a reconstruction result, so the
# connection must not pass the index straight through (it used to call
# int.imag_residual_norm and raise AttributeError).
dlg = _dialog(qapp)
other = 1 - dlg._recon_mode_combo.currentIndex()
dlg._recon_mode_combo.setCurrentIndex(other) # fires currentIndexChanged
assert dlg._recon_mode_combo.currentIndex() == other
dlg.deleteLater()


class TestPhaseView:
def test_default_is_magnitude(self, qapp):
dlg = _dialog(qapp)
Expand Down
29 changes: 29 additions & 0 deletions tests/test_processing_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1039,3 +1039,32 @@ def test_apply_processing_state_to_scan_preserves_extent_after_scale(self):
)
assert scan.planes[0].shape == (32, 24)
np.testing.assert_allclose(scan.scan_range_m, (3e-9, 5e-9))


def test_stm_background_active_roi_without_roi_set_degrades_to_whole_image():
"""A constant-Δf/STM background step fit over an ROI must not crash a
thumbnail/preview render that has no roi_set: the unresolved ROI mask
degrades to a whole-image fit instead of raising in the kernel."""

rng = np.random.default_rng(0)
arr = rng.standard_normal((24, 24)).astype(np.float64) + np.linspace(0, 5, 24)

state = ProcessingState(
[
ProcessingStep(
"stm_background",
{
"fit_region": "active_roi",
"fit_roi_id": "be8e9cc6-84b2-4e4d-9990-24bdc4476258",
"model": "linear",
"line_statistic": "median",
},
)
]
)

with pytest.warns(UserWarning, match="ROI fit mask ignored"):
out = apply_processing_state(arr, state, roi_set=None)

assert out.shape == arr.shape
assert np.isfinite(out).all()
Loading