From 0826fd14ea43f8cfdcdbf492dc667fb999acb87f Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 27 Sep 2024 14:44:21 -0400 Subject: [PATCH 1/8] add keyboard shortcuts for open and export --- src/py4D_browser/main_window.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 60f9216..738392b 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -11,6 +11,7 @@ QLabel, QToolTip, QPushButton, + QShortcut, ) from matplotlib.backend_bases import tools @@ -134,6 +135,7 @@ def setup_menus(self): self.load_auto_action = QAction("&Load Data...", self) self.load_auto_action.triggered.connect(self.load_data_auto) self.file_menu.addAction(self.load_auto_action) + self.load_auto_action.setShortcut(QtGui.QKeySequence("Ctrl+O")) self.load_mmap_action = QAction("Load &Memory Map...", self) self.load_mmap_action.triggered.connect(self.load_data_mmap) @@ -163,6 +165,8 @@ def setup_menus(self): for method in ["Raw float32", "py4DSTEM HDF5", "Plain HDF5"]: menu_item = datacube_export_menu.addAction(method) menu_item.triggered.connect(partial(self.export_datacube, method)) + if method == "py4DSTEM HDF5": + menu_item.setShortcut(QtGui.QKeySequence("Ctrl+S")) # Submenu to export virtual image vimg_export_menu = QMenu("Export Virtual Image", self) From 45d990942d323d0ce356accad1acbcd7bd0b51c7 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 27 Sep 2024 14:44:42 -0400 Subject: [PATCH 2/8] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bfd434e..86a47ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "py4D_browser" -version = "1.1.3" +version = "1.1.4" authors = [ { name="Steven Zeltmann", email="steven.zeltmann@lbl.gov" }, ] From 74d43a006f850571cefc4abf2a7ffbdb5702b096 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 30 Sep 2024 09:04:35 -0400 Subject: [PATCH 3/8] better progress bar for manual tcBF --- src/py4D_browser/dialogs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py4D_browser/dialogs.py b/src/py4D_browser/dialogs.py index 58694a2..d86352e 100644 --- a/src/py4D_browser/dialogs.py +++ b/src/py4D_browser/dialogs.py @@ -1,6 +1,7 @@ -from py4DSTEM import DataCube, data, tqdmnd +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 @@ -428,8 +429,9 @@ def reconstruct(self): qy_operator = qy_operator * -2.0j * np.pi # loop over images and shift - for mx, my in tqdmnd( - *mask.shape, + img_indices = np.argwhere(mask) + for mx, my in tqdm( + img_indices, desc="Shifting images", file=StatusBarWriter(self.parent.statusBar()), mininterval=1.0, From ae9a8ee44e296f4fbde39dff3adc9d10a936f494 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 11 Oct 2024 14:42:26 -0400 Subject: [PATCH 4/8] store some state in config file, recall on next load --- pyproject.toml | 1 + src/py4D_browser/main_window.py | 40 +++++++++++++++++++++++++++++--- src/py4D_browser/update_views.py | 2 ++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 86a47ec..86f7aab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "h5py", "numpy >= 1.19", "matplotlib >= 3.2.2", + "platformdirs", "PyQt5 >= 5.10", "pyqtgraph >= 0.11", "sigfig", diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 738392b..3dcd3bf 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -22,6 +22,7 @@ from pathlib import Path import importlib import os +import platformdirs from py4D_browser.utils import pg_point_roi, VLine, LatchingButton from py4D_browser.scalebar import ScaleBar @@ -98,6 +99,21 @@ def __init__(self, argv): self.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) + + # Reset stored state if so asked: + if os.environ.get("PY4DGUI_RESET"): + self.settings.remove("last_state") + print("Cleared saved state, using defaults...") + self.setup_menus() self.setup_views() @@ -109,7 +125,9 @@ def __init__(self, argv): font.setPointSize(10) QToolTip.setFont(font) - self.resize(1000, 800) + self.resize( + self.settings.value("last_state/window_size", QtCore.QSize(1000, 800)), + ) self.show() @@ -297,6 +315,9 @@ def setup_menus(self): diff_range_group = QActionGroup(self) diff_range_group.setExclusive(True) + scale_range_default = self.settings.value( + "last_state/diffraction_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) diff_range_group.addAction(action) @@ -306,7 +327,10 @@ def setup_menus(self): partial(self.set_diffraction_autoscale_range, scale_range) ) # set default - if scale_range[0] == 2 and scale_range[1] == 98: + if ( + scale_range[0] == scale_range_default[0] + and scale_range[1] == scale_range_default[1] + ): action.setChecked(True) self.set_diffraction_autoscale_range(scale_range, redraw=False) @@ -319,6 +343,9 @@ def setup_menus(self): vimg_range_group = QActionGroup(self) vimg_range_group.setExclusive(True) + scale_range_default = self.settings.value( + "last_state/realspace_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) vimg_range_group.addAction(action) @@ -328,7 +355,10 @@ def setup_menus(self): partial(self.set_real_space_autoscale_range, scale_range) ) # set default - if scale_range[0] == 2 and scale_range[1] == 98: + if ( + scale_range[0] == scale_range_default[0] + and scale_range[1] == scale_range_default[1] + ): action.setChecked(True) self.set_real_space_autoscale_range(scale_range, redraw=False) @@ -663,6 +693,10 @@ def setup_views(self): ) self.statusBar().addPermanentWidget(self.realspace_rescale_button) + def resizeEvent(self, event): + # Store window size for next run + self.settings.setValue("last_state/window_size", event.size()) + # Handle dragging and dropping a file on the window def dragEnterEvent(self, event): if event.mimeData().hasUrls(): diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 47ac0e1..4ddbea1 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -521,6 +521,7 @@ def update_diffraction_detector(self): def set_diffraction_autoscale_range(self, percentiles, redraw=True): self.diffraction_autoscale_percentiles = percentiles + self.settings.setValue("last_state/diffraction_autorange", list(percentiles)) if redraw: self._render_diffraction_image(reset=False) @@ -528,6 +529,7 @@ def set_diffraction_autoscale_range(self, percentiles, redraw=True): def set_real_space_autoscale_range(self, percentiles, redraw=True): self.real_space_autoscale_percentiles = percentiles + self.settings.setValue("last_state/realspace_autorange", list(percentiles)) if redraw: self._render_virtual_image(reset=False) From a931f993eff254dc059b114c292673efbc3278a0 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sat, 12 Oct 2024 10:39:41 -0400 Subject: [PATCH 5/8] minor changes to default positioning of detectors --- src/py4D_browser/update_views.py | 16 +++++++++++----- src/py4D_browser/utils.py | 4 ++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 4ddbea1..9fd94ea 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -29,6 +29,7 @@ def update_real_space_view(self, reset=False): ], detector_mode # If a CoM method is checked, ensure linear scaling + scaling_mode = self.vimg_scaling_group.checkedAction().text().replace("&", "") if detector_mode in ["CoM Magnitude", "CoM Angle"] and scaling_mode != "Linear": print("Warning! Setting linear scaling for CoM image") self.vimg_scale_linear_action.setChecked(True) @@ -383,7 +384,7 @@ def update_realspace_detector(self): return x, y = self.datacube.data.shape[:2] - x0, y0 = x / 2, y / 2 + x0, y0 = x // 2, y // 2 xr, yr = x / 10, y / 10 # Remove existing detector @@ -396,7 +397,10 @@ def update_realspace_detector(self): # Rectangular detector if detector_shape == "Point": - self.real_space_point_selector = pg_point_roi(self.real_space_widget.getView()) + self.real_space_point_selector = pg_point_roi( + self.real_space_widget.getView(), + center=(x0 - 0.5, y0 - 0.5), + ) self.real_space_point_selector.sigRegionChanged.connect( partial(self.update_diffraction_space_view, False) ) @@ -426,7 +430,7 @@ def update_diffraction_detector(self): return x, y = self.datacube.data.shape[2:] - x0, y0 = x / 2, y / 2 + x0, y0 = x // 2, y // 2 xr, yr = x / 10, y / 10 # Remove existing detector @@ -449,15 +453,17 @@ def update_diffraction_detector(self): ) self.virtual_detector_roi_outer = None - # Rectangular detector + # Point detector if detector_shape == "Point": self.virtual_detector_point = pg_point_roi( - self.diffraction_space_widget.getView() + self.diffraction_space_widget.getView(), + center=(x0 - 0.5, y0 - 0.5), ) self.virtual_detector_point.sigRegionChanged.connect( partial(self.update_real_space_view, False) ) + # Rectangular detector elif detector_shape == "Rectangular": self.virtual_detector_roi = pg.RectROI( [int(x0 - xr / 2), int(y0 - yr / 2)], [int(xr), int(yr)], pen=(3, 9) diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index 03e1d21..5ac1058 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -66,12 +66,12 @@ def on_click(self, *args): self.status_bar.showMessage("Shift+click to keep on", 5_000) -def pg_point_roi(view_box): +def pg_point_roi(view_box, center=(-0.5, -0.5)): """ Point selection. Based in pyqtgraph, and returns a pyqtgraph CircleROI object. This object has a sigRegionChanged.connect() signal method to connect to other functions. """ - circ_roi = pg.CircleROI((-0.5, -0.5), (2, 2), movable=True, pen=(0, 9)) + circ_roi = pg.CircleROI(center, (2, 2), movable=True, pen=(0, 9)) h = circ_roi.addTranslateHandle((0.5, 0.5)) h.pen = pg.mkPen("r") h.update() From 83cf72c35faf9fb7ed46ad032ca518b1db8a4801 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 21 Oct 2024 16:17:04 -0400 Subject: [PATCH 6/8] replace the old CoM magnitude and angle detectors with one that displays CoM using color wheel, update virtual image display routines to handle complex numbers --- src/py4D_browser/main_window.py | 18 ++----- src/py4D_browser/update_views.py | 89 +++++++++++++++++++------------- 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 3dcd3bf..2bbd098 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -391,19 +391,11 @@ def setup_menus(self): detector_mode_group.addAction(detector_maximum_action) self.detector_menu.addAction(detector_maximum_action) - detector_CoM_magnitude = QAction("CoM Ma&gnitude", self) - detector_CoM_magnitude.setCheckable(True) - detector_CoM_magnitude.triggered.connect( - partial(self.update_real_space_view, True) - ) - detector_mode_group.addAction(detector_CoM_magnitude) - self.detector_menu.addAction(detector_CoM_magnitude) - - detector_CoM_angle = QAction("CoM &Angle", self) - detector_CoM_angle.setCheckable(True) - detector_CoM_angle.triggered.connect(partial(self.update_real_space_view, True)) - detector_mode_group.addAction(detector_CoM_angle) - self.detector_menu.addAction(detector_CoM_angle) + detector_CoM = QAction("C&oM", self) + detector_CoM.setCheckable(True) + detector_CoM.triggered.connect(partial(self.update_real_space_view, True)) + detector_mode_group.addAction(detector_CoM) + self.detector_menu.addAction(detector_CoM) detector_iCoM = QAction("i&CoM", self) detector_iCoM.setCheckable(True) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 9fd94ea..42fc227 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -23,15 +23,14 @@ def update_real_space_view(self, reset=False): assert detector_mode in [ "Integrating", "Maximum", - "CoM Magnitude", - "CoM Angle", + "CoM", "iCoM", ], detector_mode # If a CoM method is checked, ensure linear scaling scaling_mode = self.vimg_scaling_group.checkedAction().text().replace("&", "") - if detector_mode in ["CoM Magnitude", "CoM Angle"] and scaling_mode != "Linear": - print("Warning! Setting linear scaling for CoM image") + if detector_mode == "CoM" and scaling_mode != "Linear": + self.statusBar().showMessage("Warning! Setting linear scaling for CoM image") self.vimg_scale_linear_action.setChecked(True) scaling_mode = "Linear" @@ -145,10 +144,8 @@ def update_real_space_view(self, reset=False): CoMx -= np.mean(CoMx) CoMy -= np.mean(CoMy) - if detector_mode == "CoM Magnitude": - vimg = np.hypot(CoMx, CoMy) - elif detector_mode == "CoM Angle": - vimg = np.arctan2(CoMy, CoMx) + if detector_mode == "CoM": + vimg = CoMx + 1.0j * CoMy elif detector_mode == "iCoM": dpc = py4DSTEM.process.phase.DPC(verbose=False) dpc.preprocess( @@ -175,17 +172,44 @@ def set_virtual_image(self, vimg, reset=False): def _render_virtual_image(self, reset=False): vimg = self.unscaled_realspace_image - scaling_mode = self.vimg_scaling_group.checkedAction().text().replace("&", "") - assert scaling_mode in ["Linear", "Log", "Square Root"], scaling_mode + # for 2D images, use the scaling set by the user + # for RGB (3D) images, always scale linear + if np.isrealobj(vimg): + scaling_mode = self.vimg_scaling_group.checkedAction().text().replace("&", "") + assert scaling_mode in ["Linear", "Log", "Square Root"], scaling_mode + + if scaling_mode == "Linear": + new_view = vimg.copy() + elif scaling_mode == "Log": + new_view = np.log2(np.maximum(vimg, self.LOG_SCALE_MIN_VALUE)) + elif scaling_mode == "Square Root": + new_view = np.sqrt(np.maximum(vimg, 0)) + else: + raise ValueError("Mode not recognized") - if scaling_mode == "Linear": - new_view = vimg.copy() - elif scaling_mode == "Log": - new_view = np.log2(np.maximum(vimg, self.LOG_SCALE_MIN_VALUE)) - elif scaling_mode == "Square Root": - new_view = np.sqrt(np.maximum(vimg, 0)) + auto_level = reset or self.realspace_rescale_button.latched + + self.real_space_widget.setImage( + new_view.T, + autoLevels=False, + levels=( + ( + np.percentile(new_view, self.real_space_autoscale_percentiles[0]), + np.percentile(new_view, self.real_space_autoscale_percentiles[1]), + ) + if auto_level + else None + ), + autoRange=reset, + ) else: - raise ValueError("Mode not recognized") + new_view = complex_to_Lab(vimg) + self.real_space_widget.setImage( + np.transpose(new_view, (1, 0, 2)), # flip x/y but keep RGB ordering + autoLevels=False, + levels=(0, 1), + autoRange=reset, + ) stats_text = [ f"Min:\t{vimg.min():.5g}", @@ -198,27 +222,14 @@ def _render_virtual_image(self, reset=False): for t, m in zip(stats_text, self.realspace_statistics_actions): m.setText(t) - auto_level = reset or self.realspace_rescale_button.latched - - self.real_space_widget.setImage( - new_view.T, - autoLevels=False, - levels=( - ( - np.percentile(new_view, self.real_space_autoscale_percentiles[0]), - np.percentile(new_view, self.real_space_autoscale_percentiles[1]), - ) - if auto_level - else None - ), - autoRange=reset, - ) - # Update FFT view self.unscaled_fft_image = None - fft_window = np.hanning(vimg.shape[0])[:, None] * np.hanning(vimg.shape[1])[None, :] + vimg_2D = vimg if np.isrealobj(vimg) else np.abs(vimg) + fft_window = ( + np.hanning(vimg_2D.shape[0])[:, None] * np.hanning(vimg_2D.shape[1])[None, :] + ) if self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT": - fft = np.abs(np.fft.fftshift(np.fft.fft2(vimg * fft_window))) ** 0.5 + fft = np.abs(np.fft.fftshift(np.fft.fft2(vimg_2D * fft_window))) ** 0.5 levels = (np.min(fft), np.percentile(fft, 99.9)) mode_switch = self.fft_widget_text.textItem.toPlainText() != "Virtual Image FFT" self.fft_widget_text.setText("Virtual Image FFT") @@ -234,7 +245,7 @@ def _render_virtual_image(self, reset=False): self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT (complex)" ): - fft = np.fft.fftshift(np.fft.fft2(vimg * fft_window)) + fft = np.fft.fftshift(np.fft.fft2(vimg_2D * fft_window)) levels = (np.min(np.abs(fft)), np.percentile(np.abs(fft), 99.9)) mode_switch = self.fft_widget_text.textItem.toPlainText() != "Virtual Image FFT" self.fft_widget_text.setText("Virtual Image FFT") @@ -604,7 +615,11 @@ def update_tooltip(self): y = int(np.clip(np.floor(pos_in_data.x()), 0, data.shape[0] - 1)) x = int(np.clip(np.floor(pos_in_data.y()), 0, data.shape[1] - 1)) - display_text = f"[{x},{y}]: {data[x,y]:.5g}" + + if np.isrealobj(data): + display_text = f"[{x},{y}]: {data[x,y]:.5g}" + else: + display_text = f"[{x},{y}]: |z|={np.abs(data[x,y]):.5g}, ϕ={np.degrees(np.angle(data[x,y])):.5g}°" self.cursor_value_text.setText(display_text) From d9831e415e75b505608930e00d34b7ca61d5383f Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Tue, 22 Oct 2024 10:52:12 -0400 Subject: [PATCH 7/8] write progress bar for complex detectors --- src/py4D_browser/update_views.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 42fc227..b494434 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -7,7 +7,12 @@ from PyQt5.QtGui import QCursor import os -from py4D_browser.utils import pg_point_roi, make_detector, complex_to_Lab +from py4D_browser.utils import ( + pg_point_roi, + make_detector, + complex_to_Lab, + StatusBarWriter, +) def update_real_space_view(self, reset=False): @@ -119,7 +124,12 @@ def update_real_space_view(self, reset=False): return mask = mask.astype(np.float32) vimg = np.zeros((self.datacube.R_Nx, self.datacube.R_Ny)) - iterator = py4DSTEM.tqdmnd(self.datacube.R_Nx, self.datacube.R_Ny, disable=True) + iterator = py4DSTEM.tqdmnd( + self.datacube.R_Nx, + self.datacube.R_Ny, + file=StatusBarWriter(self.statusBar()), + mininterval=0.1, + ) if detector_mode == "Integrating": for rx, ry in iterator: From 00044b4c6b1fdaccac668ad0611e3a96fb96ceee Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Tue, 22 Oct 2024 10:52:48 -0400 Subject: [PATCH 8/8] new minor version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 86f7aab..2eb1b60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "py4D_browser" -version = "1.1.4" +version = "1.2.0" authors = [ { name="Steven Zeltmann", email="steven.zeltmann@lbl.gov" }, ]