Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 0.9999 #7

Merged
merged 21 commits into from
Dec 4, 2023
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
14 changes: 14 additions & 0 deletions .github/workflows/black.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Check code style

on:
push:
branches: [ "dev" ]
pull_request:
branches: [ "dev" ]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: psf/black@stable
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
This repository hosts the `pyqt` based graphical 4D--STEM data browser that was originally part of **py4DSTEM** until version 0.13.11.

## Installation
The GUI is available on PyPI: `pip install py4D-browser`
`conda` installation will be available shortly.
The GUI is available on PyPI and conda-forge:

`pip install py4D-browser`

`conda install -c conda-forge py4d-browser`


## Usage
Run `py4DGUI` in your terminal to open the GUI. Then just drag and drop a 4D-STEM dataset into the window!
Expand Down
10 changes: 7 additions & 3 deletions 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 = "0.99"
version = "0.9999"
authors = [
{ name="Steven Zeltmann", email="[email protected]" },
]
Expand All @@ -16,7 +16,8 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"py4dstem >= 0.14.0",
"py4dstem >= 0.14.3",
"emdfile >= 0.0.11",
"numpy >= 1.19",
"matplotlib >= 3.2.2",
"PyQt5 >= 5.10",
Expand All @@ -28,4 +29,7 @@ py4DGUI = "py4D_browser.runGUI:launch"

[project.urls]
"Homepage" = "https://github.com/py4dstem/py4D-browser"
"Bug Tracker" = "https://github.com/py4dstem/py4D-browser/issues"
"Bug Tracker" = "https://github.com/py4dstem/py4D-browser/issues"

[tool.pyright]
venv = "py4dstem"
49 changes: 31 additions & 18 deletions src/py4D_browser/main_window.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
from PyQt5.QtCore import Qt
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QWidget,
QMenu,
QAction,
QFileDialog,
QVBoxLayout,
QHBoxLayout,
QFrame,
QPushButton,
QScrollArea,
QCheckBox,
QLineEdit,
QRadioButton,
QButtonGroup,
QDesktopWidget,
QMessageBox,
QSplitter,
QActionGroup,
)
from PyQt5 import QtGui

import pyqtgraph as pg
import numpy as np
Expand Down Expand Up @@ -81,6 +69,10 @@ def __init__(self, argv):

self.show()

# If a file was passed on the command line, open it
if len(argv) > 1:
self.load_file(argv[1])

def setup_menus(self):
self.menu_bar = self.menuBar()

Expand Down Expand Up @@ -229,7 +221,7 @@ def setup_menus(self):

detector_point_action = QAction("&Point", self)
detector_point_action.setCheckable(True)
detector_point_action.setChecked(True) # Default
detector_point_action.setChecked(True) # Default
detector_point_action.triggered.connect(self.update_diffraction_detector)
detector_shape_group.addAction(detector_point_action)
self.detector_shape_menu.addAction(detector_point_action)
Expand Down Expand Up @@ -263,7 +255,9 @@ def setup_views(self):
self.diffraction_space_widget.addItem(self.diffraction_space_view_text)

# Create virtual detector ROI selector
self.virtual_detector_point = pg_point_roi(self.diffraction_space_widget.getView())
self.virtual_detector_point = pg_point_roi(
self.diffraction_space_widget.getView()
)
self.virtual_detector_point.sigRegionChanged.connect(
self.update_real_space_view
)
Expand Down Expand Up @@ -300,9 +294,29 @@ def setup_views(self):
self.diffraction_space_widget.dropEvent = self.dropEvent
self.real_space_widget.dropEvent = self.dropEvent

# Set up the FFT window.
self.fft_widget = pg.ImageView()
self.fft_widget.setImage(np.zeros((512, 512)))

# Name and return
self.fft_widget.setWindowTitle("FFT of Virtual Image")
self.fft_widget.addItem(pg.TextItem("FFT", (200, 200, 200), None, (0, 1)))

self.fft_widget.setAcceptDrops(True)
self.fft_widget.dragEnterEvent = self.dragEnterEvent
self.fft_widget.dropEvent = self.dropEvent

layout = QHBoxLayout()
layout.addWidget(self.diffraction_space_widget, 1)
layout.addWidget(self.real_space_widget, 1)

# add a resizeable layout for the vimg and FFT
rightside = QSplitter()
rightside.addWidget(self.real_space_widget)
rightside.addWidget(self.fft_widget)
rightside.setOrientation(QtCore.Qt.Vertical)
rightside.setStretchFactor(0, 2)
layout.addWidget(rightside, 1)

widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
Expand All @@ -319,4 +333,3 @@ def dropEvent(self, event):
if len(files) == 1:
print(f"Reieving dropped file: {files[0]}")
self.load_file(files[0])

8 changes: 4 additions & 4 deletions src/py4D_browser/menu_actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import py4DSTEM
from PyQt5.QtWidgets import QFileDialog
import h5py
import os


def load_data_auto(self):
Expand All @@ -21,9 +22,8 @@ def load_data_bin(self):

def load_file(self, filepath, mmap=False, binning=1):
print(f"Loading file {filepath}")

from py4DSTEM.io.parsefiletype import _parse_filetype
if _parse_filetype(filepath) == "H5":
print(f"Type: {os.path.splitext(filepath)[-1].lower()}")
if os.path.splitext(filepath)[-1].lower() in (".h5", ".hdf5", ".py4dstem", ".emd"):
datacubes = get_4D(h5py.File(filepath, "r"))
print(f"Found {len(datacubes)} 4D datasets inside the HDF5 file...")
if len(datacubes) >= 1:
Expand All @@ -50,7 +50,7 @@ def show_file_dialog(self):
self,
"Open 4D-STEM Data",
"",
"4D-STEM Data (*.dm3 *.dm4 *.raw *.mib *.gtg);;Any file (*)",
"4D-STEM Data (*.dm3 *.dm4 *.raw *.mib *.gtg *.h5 *.hdf5 *.emd *.py4dstem);;Any file (*)",
)
if filename is not None and len(filename[0]) > 0:
return filename[0]
Expand Down
74 changes: 43 additions & 31 deletions src/py4D_browser/update_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
import numpy as np
import py4DSTEM

from py4D_browser.utils import pg_point_roi
from py4D_browser.utils import pg_point_roi, make_detector


def update_real_space_view(self, reset=False):
scaling_mode = self.vimg_scaling_group.checkedAction().text().replace("&", "")
assert scaling_mode in ["Linear", "Log", "Square Root"], scaling_mode

detector_shape = self.detector_shape_group.checkedAction().text().replace("&", "")
assert detector_shape in ["Point", "Rectangular", "Circle", "Annulus"], detector_shape
assert detector_shape in [
"Point",
"Rectangular",
"Circle",
"Annulus",
], detector_shape

detector_mode = self.detector_mode_group.checkedAction().text().replace("&", "")
assert detector_mode in [
Expand Down Expand Up @@ -55,37 +60,34 @@ def update_real_space_view(self, reset=False):
mask[slice_x, slice_y] = True

elif detector_shape == "Circle":
(slice_x, slice_y), _ = self.virtual_detector_roi.getArraySlice(
self.datacube.data[0, 0, :, :], self.diffraction_space_widget.getImageItem()
)
x0 = (slice_x.start + slice_x.stop) / 2.0
y0 = (slice_y.start + slice_y.stop) / 2.0
R = (slice_y.stop - slice_y.start) / 2.0
R = self.virtual_detector_roi.size()[0] / 2.0

self.diffraction_space_view_text.setText(f"[({x0},{y0}),{R}]")
x0 = self.virtual_detector_roi.pos()[0] + R
y0 = self.virtual_detector_roi.pos()[1] + R

mask = py4DSTEM.process.virtualimage.make_detector(
self.diffraction_space_view_text.setText(f"[({x0:.0f},{y0:.0f}),{R:.0f}]")

mask = make_detector(
(self.datacube.Q_Nx, self.datacube.Q_Ny), "circle", ((x0, y0), R)
)
elif detector_shape == "Annulus":
(slice_x, slice_y), _ = self.virtual_detector_roi_outer.getArraySlice(
self.datacube.data[0, 0, :, :], self.diffraction_space_widget.getImageItem()
)
x0 = (slice_x.start + slice_x.stop) / 2.0
y0 = (slice_y.start + slice_y.stop) / 2.0
R_outer = (slice_y.stop - slice_y.start) / 2.0
inner_pos = self.virtual_detector_roi_inner.pos()
inner_size = self.virtual_detector_roi_inner.size()
R_inner = inner_size[0] / 2.0
x0 = inner_pos[0] + R_inner
y0 = inner_pos[1] + R_inner

(slice_ix, slice_iy), _ = self.virtual_detector_roi_inner.getArraySlice(
self.datacube.data[0, 0, :, :], self.diffraction_space_widget.getImageItem()
)
R_inner = (slice_iy.stop - slice_iy.start) / 2.0
outer_size = self.virtual_detector_roi_outer.size()
R_outer = outer_size[0] / 2.0

if R_inner == R_outer:
if R_inner <= R_outer:
R_inner -= 1

self.diffraction_space_view_text.setText(f"[({x0},{y0}),({R_inner},{R_outer})]")
self.diffraction_space_view_text.setText(
f"[({x0:.0f},{y0:.0f}),({R_inner:.0f},{R_outer:.0f})]"
)

mask = py4DSTEM.process.virtualimage.make_detector(
mask = make_detector(
(self.datacube.Q_Nx, self.datacube.Q_Ny),
"annulus",
((x0, y0), (R_inner, R_outer)),
Expand All @@ -99,14 +101,19 @@ def update_real_space_view(self, reset=False):
# Normalize coordinates
xc = np.clip(xc, 0, self.datacube.Q_Nx - 1)
yc = np.clip(yc, 0, self.datacube.Q_Ny - 1)
vimg = self.datacube.data[: ,: , xc, yc]
vimg = self.datacube.data[:, :, xc, yc]

self.diffraction_space_view_text.setText(f"[{xc},{yc}]")

else:
raise ValueError("Detector shape not recognized")

if mask is not None:
# For debugging masks:
# self.diffraction_space_widget.setImage(
# mask.T, autoLevels=True, autoRange=True
# )
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)

Expand Down Expand Up @@ -155,6 +162,11 @@ def update_real_space_view(self, reset=False):
raise ValueError("Mode not recognized")
self.real_space_widget.setImage(new_view.T, autoLevels=True)

# Update FFT view
fft = np.abs(np.fft.fftshift(np.fft.fft2(new_view))) ** 0.5
levels = (np.min(fft), np.percentile(fft, 99.9))
self.fft_widget.setImage(fft.T, autoLevels=False, levels=levels, autoRange=reset)


def update_diffraction_space_view(self, reset=False):
scaling_mode = self.diff_scaling_group.checkedAction().text().replace("&", "")
Expand Down Expand Up @@ -204,7 +216,9 @@ def update_diffraction_detector(self):

# Remove existing detector
if hasattr(self, "virtual_detector_point"):
self.diffraction_space_widget.view.scene().removeItem(self.virtual_detector_point)
self.diffraction_space_widget.view.scene().removeItem(
self.virtual_detector_point
)
if hasattr(self, "virtual_detector_roi"):
self.diffraction_space_widget.view.scene().removeItem(self.virtual_detector_roi)
if hasattr(self, "virtual_detector_roi_inner"):
Expand All @@ -218,7 +232,9 @@ def update_diffraction_detector(self):

# Rectangular detector
if detector_shape == "Point":
self.virtual_detector_point = pg_point_roi(self.diffraction_space_widget.getView())
self.virtual_detector_point = pg_point_roi(
self.diffraction_space_widget.getView()
)
self.virtual_detector_point.sigRegionChanged.connect(
self.update_real_space_view
)
Expand Down Expand Up @@ -279,11 +295,7 @@ def update_diffraction_detector(self):
)

else:
raise ValueError(
"Unknown detector shape! Got: {}".format(
detector_shape
)
)
raise ValueError("Unknown detector shape! Got: {}".format(detector_shape))

self.update_real_space_view()

Expand Down
48 changes: 47 additions & 1 deletion src/py4D_browser/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import pyqtgraph as pg
import numpy as np


def pg_point_roi(view_box):
"""
Expand All @@ -11,4 +13,48 @@ def pg_point_roi(view_box):
h.update()
view_box.addItem(circ_roi)
circ_roi.removeHandle(0)
return circ_roi
return circ_roi


def make_detector(shape: tuple, mode: str, geometry) -> np.ndarray:
match mode, geometry:
case ["point", (qx, qy)]:
mask = np.zeros(shape, dtype=np.bool_)
mask[qx, qy] = True
case ["point", geom]:
raise ValueError(
f"Point detector shape must be specified as (qx,qy), not {geom}"
)

case [("circle" | "circular"), ((qx, qy), r)]:
ix, iy = np.indices(shape)
mask = np.hypot(ix - qx, iy - qy) <= r
case [("circle" | "circular"), geom]:
raise ValueError(
f"Circular detector shape must be specified as ((qx,qy),r), not {geom}"
)

case [("annulus" | "annular"), ((qx, qy), (ri, ro))]:
ix, iy = np.indices(shape)
ir = np.hypot(ix - qx, iy - qy)
mask = np.logical_and(ir >= ri, ir <= ro)
case [("annulus" | "annular"), geom]:
raise ValueError(
f"Annular detector shape must be specified as ((qx,qy),(ri,ro)), not {geom}"
)

case [("rectangle" | "square" | "rectangular"), (xmin, xmax, ymin, ymax)]:
mask = np.zeros(shape, dtype=np.bool_)
mask[xmin:xmax, ymin:ymax] = True
case [("rectangle" | "square" | "rectangular"), geom]:
raise ValueError(
f"Rectangular detector shape must be specified as (xmin,xmax,ymin,ymax), not {geom}"
)

case ["mask", mask_arr]:
mask = mask_arr

case unknown:
raise ValueError(f"mode and geometry not understood: {unknown}")

return mask