Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
44e919d
display error dialog when saving a datacube fails
sezelt Dec 19, 2025
d08656c
add plugin to set logging levels
sezelt Dec 19, 2025
3ce14ac
delete extraneous line
sezelt Dec 19, 2025
df96d3a
add metadata viewer, fix metadata reading for emdfile data
sezelt Dec 19, 2025
5c4091a
bump version
sezelt Dec 19, 2025
d2348ce
better error formatting
jwasys Jan 14, 2026
da76ad2
copy to clipboard
sezelt Feb 2, 2026
536aaa7
show message that file is saved
sezelt Apr 1, 2026
6f1a552
show config file from Help
sezelt Apr 17, 2026
61941d0
mostly migrate FFT window to generic result window
sezelt Apr 17, 2026
a55c7d6
add result handler cleanup function
sezelt Apr 20, 2026
f406424
add same scaling options for result window as main views
sezelt Apr 20, 2026
446fbe3
postinit mechanics for plugins
sezelt Apr 20, 2026
5759e4b
fft view bugfixes
sezelt Apr 20, 2026
b53a306
fftshift for complex FFT view
sezelt Apr 20, 2026
875513f
actually emit datacube changed signal
sezelt Apr 21, 2026
a1f422b
various static analysis changes
sezelt Apr 23, 2026
8e6dea0
quote the static-only imports
sezelt Apr 23, 2026
171d111
calibrate accelerating voltage
sezelt Apr 23, 2026
2ec6775
more type hints
sezelt Apr 23, 2026
3555bf5
Merge remote-tracking branch 'py4dstem/dev' into dev
sezelt Apr 23, 2026
14f693a
optional alternate logo
sezelt Apr 28, 2026
3b4db70
add export for result view
sezelt Apr 28, 2026
c51ee09
raw data tooltip
sezelt Apr 28, 2026
fa6881c
format with black
sezelt Apr 29, 2026
28417f8
better sizing of metadata plugin
sezelt Apr 29, 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
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "py4D_browser"
version = "1.3.1"
version = "1.5.0"
authors = [
{ name="Steven Zeltmann", email="steven.zeltmann@lbl.gov" },
]
Expand All @@ -25,6 +25,7 @@ dependencies = [
"PyQt5 >= 5.10",
"pyqtgraph >= 0.11",
"sigfig",
"show-in-file-manager",
]

[project.scripts]
Expand Down
15 changes: 1 addition & 14 deletions src/py4D_browser/dialogs.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
from py4DSTEM import DataCube, data
import pyqtgraph as pg
import numpy as np
from tqdm import tqdm
from PyQt5.QtWidgets import QFrame, QPushButton, QApplication, QLabel
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import Qt, QObject
from PyQt5.QtGui import QDoubleValidator
from PyQt5.QtWidgets import QPushButton, QLabel
from PyQt5.QtWidgets import (
QDialog,
QHBoxLayout,
QVBoxLayout,
QSpinBox,
QLineEdit,
QComboBox,
QGroupBox,
QGridLayout,
QCheckBox,
)
from py4D_browser.utils import make_detector, StatusBarWriter


class ResizeDialog(QDialog):
Expand Down
2 changes: 1 addition & 1 deletion src/py4D_browser/help_menu.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from PyQt5 import QtGui, QtCore
from PyQt5 import QtGui
from PyQt5.QtWidgets import (
QWidget,
QDialog,
Expand Down
Binary file added src/py4D_browser/logo_alternate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
201 changes: 168 additions & 33 deletions src/py4D_browser/main_window.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Optional
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import (
QApplication,
Expand All @@ -14,17 +15,17 @@
QShortcut,
)

from matplotlib.backend_bases import tools
from py4DSTEM import DataCube
import pyqtgraph as pg
import numpy as np

from functools import partial
from pathlib import Path
import importlib
import os, sys
import os
import platformdirs
from showinfm import show_in_file_manager

from py4D_browser.utils import pg_point_roi, VLine, LatchingButton
from py4D_browser.utils import VLine, LatchingButton, strtobool
from py4D_browser.scalebar import ScaleBar


Expand All @@ -51,57 +52,85 @@ class DataViewer(QMainWindow):
reshape_data,
set_datacube,
update_scalebars,
copy_vimg_to_clipboard,
copy_diff_to_clipboard,
copy_result_to_clipboard,
)

from py4D_browser.update_views import (
set_virtual_image,
set_diffraction_image,
set_result_image,
get_diffraction_detector,
get_virtual_image_detector,
_render_virtual_image,
_render_diffraction_image,
_render_result_image,
update_diffraction_space_view,
update_real_space_view,
update_fft_view,
update_realspace_detector,
update_diffraction_detector,
set_diffraction_autoscale_range,
set_real_space_autoscale_range,
set_result_autoscale_range,
nudge_real_space_selector,
nudge_diffraction_selector,
update_annulus_pos,
update_annulus_radii,
update_tooltip,
)

from py4D_browser.signals import (
register_result_callback,
set_internal_result_callback,
)

from py4D_browser.plugins import load_plugins

signal_diffraction_data_changed = QtCore.pyqtSignal()
signal_virtual_image_data_changed = QtCore.pyqtSignal()
signal_datacube_changed = QtCore.pyqtSignal()

def __init__(self, argv):
super().__init__()
# Define this as the QApplication object
self.qtapp = QApplication.instance()
if not self.qtapp:
self.qtapp = QApplication(argv)

# Load settings from config file
self.config_path = os.path.join(
platformdirs.user_config_dir("py4DGUI", "py4DSTEM"), "GUI_config.ini"
)
print(f"Loading configuration from {self.config_path}")
QtCore.QCoreApplication.setOrganizationName("py4DSTEM")
QtCore.QCoreApplication.setOrganizationDomain("py4DSTEM.com")
QtCore.QCoreApplication.setApplicationName("py4DGUI")
self.settings = QtCore.QSettings(
self.config_path, QtCore.QSettings.Format.IniFormat
)

self.setWindowTitle("py4DSTEM")

icon = QtGui.QIcon(str(Path(__file__).parent.absolute() / "logo.png"))
alternate_logo = strtobool(self.settings.value("gui/quack", "0"))
icon = QtGui.QIcon(
str(
Path(__file__).parent.absolute()
/ ("logo.png" if not alternate_logo else "logo_alternate.png")
)
)
self.setWindowIcon(icon)
self.qtapp.setWindowIcon(icon)

self.setWindowTitle("py4DSTEM")
self.setAcceptDrops(True)

self.datacube = None
self.datacube: Optional[DataCube] = None

# Load settings from cofig file
config_path = os.path.join(
platformdirs.user_config_dir("py4DGUI", "py4DSTEM"), "GUI_config.ini"
)
print(f"Loading configuration from {config_path}")
QtCore.QCoreApplication.setOrganizationName("py4DSTEM")
QtCore.QCoreApplication.setOrganizationDomain("py4DSTEM.com")
QtCore.QCoreApplication.setApplicationName("py4DGUI")
self.settings = QtCore.QSettings(config_path, QtCore.QSettings.Format.IniFormat)
self.unscaled_diffraction_image: Optional[np.ndarray] = None
self.unscaled_realspace_image: Optional[np.ndarray] = None
self.unscaled_fft_image: Optional[np.ndarray] = None

# Reset stored state if so asked:
if os.environ.get("PY4DGUI_RESET"):
Expand Down Expand Up @@ -134,7 +163,7 @@ def __init__(self, argv):

# launch pyqtgraph's debug console if environment variable exists
if os.environ.get("PY4DGUI_DEBUG"):
pg.dbg()
pg.dbg(namespace={"main_window": self})

def setup_menus(self):
self.menu_bar = self.menuBar()
Expand Down Expand Up @@ -186,6 +215,9 @@ def setup_menus(self):
# Submenu to export virtual image
vimg_export_menu = QMenu("Export Virtual Image", self)
self.file_menu.addMenu(vimg_export_menu)
menu_item = vimg_export_menu.addAction("To clipboard")
menu_item.triggered.connect(self.copy_vimg_to_clipboard)
menu_item.setShortcut(QtGui.QKeySequence("Ctrl+C"))
for method in ["PNG (display)", "TIFF (display)", "TIFF (raw)"]:
menu_item = vimg_export_menu.addAction(method)
menu_item.triggered.connect(
Expand All @@ -195,12 +227,26 @@ def setup_menus(self):
# Submenu to export diffraction
vdiff_export_menu = QMenu("Export Diffraction Pattern", self)
self.file_menu.addMenu(vdiff_export_menu)
menu_item = vdiff_export_menu.addAction("To clipboard")
menu_item.triggered.connect(self.copy_diff_to_clipboard)
menu_item.setShortcut(QtGui.QKeySequence("Ctrl+Alt+C"))
for method in ["PNG (display)", "TIFF (display)", "TIFF (raw)"]:
menu_item = vdiff_export_menu.addAction(method)
menu_item.triggered.connect(
partial(self.export_virtual_image, method, "diffraction")
)

result_export_menu = QMenu("Export Result", self)
self.file_menu.addMenu(result_export_menu)
menu_item = result_export_menu.addAction("To clipboard")
menu_item.triggered.connect(self.copy_result_to_clipboard)
menu_item.setShortcut(QtGui.QKeySequence("Ctrl+Shift+C"))
for method in ["PNG (display)", "TIFF (display)", "TIFF (raw)"]:
menu_item = result_export_menu.addAction(method)
menu_item.triggered.connect(
partial(self.export_virtual_image, method, "result")
)

# Scaling Menu
self.scaling_menu = QMenu("&Scaling", self)
self.menu_bar.addMenu(self.scaling_menu)
Expand Down Expand Up @@ -275,6 +321,43 @@ def setup_menus(self):
vimg_scaling_group.addAction(vimg_scale_sqrt_action)
self.scaling_menu.addAction(vimg_scale_sqrt_action)

self.scaling_menu.addSeparator()

# Real space scaling
result_scaling_group = QActionGroup(self)
result_scaling_group.setExclusive(True)
self.result_scaling_group = result_scaling_group

result_menu_separator = QAction("Result", self)
result_menu_separator.setDisabled(True)
self.scaling_menu.addAction(result_menu_separator)

result_scale_linear_action = QAction("Linear", self)
self.result_scale_linear_action = result_scale_linear_action # Save this one!
result_scale_linear_action.setCheckable(True)
result_scale_linear_action.triggered.connect(
partial(self._render_result_image, True)
)
result_scaling_group.addAction(result_scale_linear_action)
self.scaling_menu.addAction(result_scale_linear_action)

result_scale_log_action = QAction("Log", self)
result_scale_log_action.setCheckable(True)
result_scale_log_action.triggered.connect(
partial(self._render_result_image, True)
)
result_scaling_group.addAction(result_scale_log_action)
self.scaling_menu.addAction(result_scale_log_action)

result_scale_sqrt_action = QAction("Square Root", self)
result_scale_sqrt_action.setCheckable(True)
result_scale_sqrt_action.setChecked(True)
result_scale_sqrt_action.triggered.connect(
partial(self._render_result_image, True)
)
result_scaling_group.addAction(result_scale_sqrt_action)
self.scaling_menu.addAction(result_scale_sqrt_action)

# Autorange menu
self.autorange_menu = QMenu("&Autorange", self)
self.menu_bar.addMenu(self.autorange_menu)
Expand Down Expand Up @@ -333,6 +416,36 @@ def setup_menus(self):
action.setChecked(True)
self.set_real_space_autoscale_range(scale_range, redraw=False)

##
self.autorange_menu.addSeparator()

result_autoscale_separator = QAction("Result", self)
result_autoscale_separator.setDisabled(True)
self.autorange_menu.addAction(result_autoscale_separator)

result_range_group = QActionGroup(self)
result_range_group.setExclusive(True)

scale_range_default = self.settings.value(
"last_state/result_autorange", [0.1, 99.9], type=float
)
for scale_range in [(0, 100), (0.1, 99.9), (1, 99), (2, 98), (5, 95)]:
action = QAction(f"{scale_range[0]}% – {scale_range[1]}%", self)
result_range_group.addAction(action)
self.autorange_menu.addAction(action)
action.setCheckable(True)
action.triggered.connect(
partial(self.set_result_autoscale_range, scale_range)
)
# set default
if (
scale_range[0] == scale_range_default[0]
and scale_range[1] == scale_range_default[1]
):
action.setChecked(True)
self.set_result_autoscale_range(scale_range, redraw=False)
##

# Detector Response Menu
self.detector_menu = QMenu("&Detector Response", self)
self.menu_bar.addMenu(self.detector_menu)
Expand Down Expand Up @@ -474,33 +587,39 @@ def setup_menus(self):
rs_detector_shape_group.addAction(detector_rectangle_action)
self.detector_shape_menu.addAction(detector_rectangle_action)

self.fft_menu = QMenu("FF&T View", self)
self.menu_bar.addMenu(self.fft_menu)
self.result_menu = QMenu("Resul&t View", self)
self.menu_bar.addMenu(self.result_menu)

self.fft_source_action_group = QActionGroup(self)
self.fft_source_action_group.setExclusive(True)
self.result_source_action_group = QActionGroup(self)
self.result_source_action_group.setExclusive(True)
img_fft_action = QAction("Virtual Image FFT", self)
img_fft_action.setCheckable(True)
img_fft_action.triggered.connect(self.set_internal_result_callback)
img_fft_action.setChecked(True)
img_fft_action.triggered.connect(partial(self.update_real_space_view, False))
self.fft_menu.addAction(img_fft_action)
self.fft_source_action_group.addAction(img_fft_action)
self.result_menu.addAction(img_fft_action)
self.result_source_action_group.addAction(img_fft_action)

img_complex_fft_action = QAction("Virtual Image FFT (complex)", self)
img_complex_fft_action.setCheckable(True)
self.fft_menu.addAction(img_complex_fft_action)
self.fft_source_action_group.addAction(img_complex_fft_action)
img_complex_fft_action.triggered.connect(
partial(self.update_real_space_view, False)
)
self.result_menu.addAction(img_complex_fft_action)
self.result_source_action_group.addAction(img_complex_fft_action)
img_complex_fft_action.triggered.connect(self.set_internal_result_callback)

img_ewpc_action = QAction("EWPC", self)
img_ewpc_action.setCheckable(True)
self.fft_menu.addAction(img_ewpc_action)
self.fft_source_action_group.addAction(img_ewpc_action)
img_ewpc_action.triggered.connect(
partial(self.update_diffraction_space_view, False)
)
self.result_menu.addAction(img_ewpc_action)
self.result_source_action_group.addAction(img_ewpc_action)
img_ewpc_action.triggered.connect(self.set_internal_result_callback)

# action only for information purposes, to show when a plugin has taken over
# the result window
self.result_other_action = QAction("Plugin", self)
self.result_other_action.setCheckable(True)
self.result_other_action.setEnabled(False)
self.result_menu.addAction(self.result_other_action)
self.result_source_action_group.addAction(self.result_other_action)

self.set_internal_result_callback()

# Plugins menu
self.processing_menu = QMenu("&Plugins", self)
Expand All @@ -514,6 +633,12 @@ def setup_menus(self):
self.keyboard_map_action.triggered.connect(self.show_keyboard_map)
self.help_menu.addAction(self.keyboard_map_action)

self.show_config_file_action = QAction("Show Configuration File", self)
self.show_config_file_action.triggered.connect(
partial(show_in_file_manager, self.config_path)
)
self.help_menu.addAction(self.show_config_file_action)

def setup_views(self):
# Set up the diffraction space window.
self.diffraction_space_widget = pg.ImageView()
Expand Down Expand Up @@ -633,6 +758,7 @@ def setup_views(self):
self.statusBar().addPermanentWidget(VLine())
self.statusBar().addPermanentWidget(self.real_space_view_text)
self.statusBar().addPermanentWidget(VLine())

self.diffraction_rescale_button = LatchingButton(
"Autorange Diffraction",
status_bar=self.statusBar(),
Expand All @@ -642,6 +768,7 @@ def setup_views(self):
self.diffraction_space_widget.autoLevels
)
self.statusBar().addPermanentWidget(self.diffraction_rescale_button)

self.realspace_rescale_button = LatchingButton(
"Autorange Virtual Image",
status_bar=self.statusBar(),
Expand All @@ -652,6 +779,14 @@ def setup_views(self):
)
self.statusBar().addPermanentWidget(self.realspace_rescale_button)

self.result_rescale_button = LatchingButton(
"Autorange Result",
status_bar=self.statusBar(),
latched=True,
)
self.result_rescale_button.activated.connect(self.fft_widget.autoLevels)
self.statusBar().addPermanentWidget(self.result_rescale_button)

def resizeEvent(self, event):
# Store window size for next run
self.settings.setValue("last_state/window_size", event.size())
Expand Down
Loading
Loading