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 1.0 #15

Merged
merged 13 commits into from
Apr 2, 2024
2 changes: 1 addition & 1 deletion .github/workflows/black.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: psf/black@stable
- uses: psf/black@stable
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
# - id: trailing-whitespace
# - id: end-of-file-fixer
# - id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 24.2.0
hooks:
- id: black
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cff-version: 1.1.0
message: "If you use this software, please cite the accompanying paper."
abstract: "Scanning transmission electron microscopy (STEM) allows for imaging, diffraction, and spectroscopy of materials on length scales ranging from microns to atoms. By using a high-speed, direct electron detector, it is now possible to record a full two-dimensional (2D) image of the diffracted electron beam at each probe position, typically a 2D grid of probe positions. These 4D-STEM datasets are rich in information, including signatures of the local structure, orientation, deformation, electromagnetic fields, and other sample-dependent properties. However, extracting this information requires complex analysis pipelines that include data wrangling, calibration, analysis, and visualization, all while maintaining robustness against imaging distortions and artifacts. In this paper, we present py4DSTEM, an analysis toolkit for measuring material properties from 4D-STEM datasets, written in the Python language and released with an open-source license. We describe the algorithmic steps for dataset calibration and various 4D-STEM property measurements in detail and present results from several experimental datasets. We also implement a simple and universal file format appropriate for electron microscopy data in py4DSTEM, which uses the open-source HDF5 standard. We hope this tool will benefit the research community and help improve the standards for data and computational methods in electron microscopy, and we invite the community to contribute to this ongoing project."
authors:
authors:
-
affiliation: "National Center for Electron Microscopy, Molecular Foundry, Lawrence Berkeley National Laboratory, 1 Cyclotron Road, Berkeley, CA 94720, USA"
family-names: Savitzky
Expand Down
1 change: 0 additions & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -672,4 +672,3 @@ may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

# The `py4DSTEM` GUI

This repository hosts the `pyqt` based graphical 4D--STEM data browser that was originally part of **py4DSTEM** until version 0.13.11.
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 and conda-forge:
## Installation
The GUI is available on PyPI and conda-forge:

`pip install py4D-browser`

Expand All @@ -15,19 +15,19 @@ The GUI is available on PyPI and conda-forge:
Run `py4DGUI` in your terminal to open the GUI. Then just drag and drop a 4D-STEM dataset into the window!

### Controls
* Move the virtual detector and the real-space selector using the mouse or using the keyboard shortcuts: WASD moves the detector and IJKL moves the selector, and holding down shift moves 5 pixels at a time.
* Auto scaling of both views is on by default. Press the "Autoscale" buttons in the bottom right to disable. Press either button to apply automatic scaling once, or Shift + click to lock autoscaling back on.
* Move the virtual detector and the real-space selector using the mouse or using the keyboard shortcuts: WASD moves the detector and IJKL moves the selector, and holding down shift moves 5 pixels at a time.
* Auto scaling of both views is on by default. Press the "Autoscale" buttons in the bottom right to disable. Press either button to apply automatic scaling once, or Shift + click to lock autoscaling back on.
* Different shapes of virtual detector are available in the "Detector Shape" menu, and different detector responses are available in the "Detector Response" menu.
* The information in the bottom bar contains the details of the virtual detector used to generate the images, and can be entered into py4DSTEM to generate the same image.
* The information in the bottom bar contains the details of the virtual detector used to generate the images, and can be entered into py4DSTEM to generate the same image.
* The FFT pane can be switched between displaying the FFT of the virtual image and displaying the [exit wave power cepstrum](https://doi.org/10.1016/j.ultramic.2020.112994).
* Virtual images can be exported either as the scaled and clipped displays shown in the GUI or as raw data. The exact datatype stored in the raw TIFF image depends on both the datatype of the dataset and the type of virtual image being displayed (in particular, integer datatypes are converted internally to floating point to prevent overflows when generating any synthesized virtual images).
* If the [EMPAD-G2 Raw Reader](https://github.com/sezelt/empad2) is installed in the same environment, an extra menu will appear that allows the concatenated binary format data to be background subtracted and calibrated in the GUI. You can also save the calibrated data as an HDF5 file for later analysis.
* If the [EMPAD-G2 Raw Reader](https://github.com/sezelt/empad2) is installed in the same environment, an extra menu will appear that allows the concatenated binary format data to be background subtracted and calibrated in the GUI. You can also save the calibrated data as an HDF5 file for later analysis.

![Demonstration](/images/demo.gif)

The keyboard map in the Help menu was made using [this tool](https://archie-adams.github.io/keyboard-shortcut-map-maker/) and the map file is in the top level of this repo.

## About
## About

![py4DSTEM logo](/images/py4DSTEM_logo.png)

Expand Down
16 changes: 8 additions & 8 deletions py4DGUI-keymap.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

.ulButtons ul {
margin: 0;
list-style-type: none;
text-align: center;
list-style-type: none;
text-align: center;
background-color: #b4b3bd;
}
.ulButtons ul li {
Expand Down Expand Up @@ -51,7 +51,7 @@
}

.bodyStyle {
width:1109px;
width:1109px;
margin:0 auto;
}

Expand All @@ -68,8 +68,8 @@

/* Footer styling. */
footer {
background-color: #b4b3bd;
margin-top: 20px;
background-color: #b4b3bd;
margin-top: 20px;
margin-bottom: 0;
}

Expand All @@ -79,12 +79,12 @@

.footer-div {
width:960px;
margin:0 auto;
margin:0 auto;
background-color: #b4b3bd;
}

#to-top-button{
margin-left:426px;
margin-left:426px;
padding-top: 14px;
}
#to-top-input {
Expand Down Expand Up @@ -689,4 +689,4 @@ <h3 style="float: left;" class="editable">Keyboard Shortcuts</h3>
</div>

</div> <!-- End of keyboard. -->
</li></ol></div></body></html>
</li></ol></div></body></html>
4 changes: 2 additions & 2 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.999999"
version = "1.0.0"
authors = [
{ name="Steven Zeltmann", email="[email protected]" },
]
Expand Down Expand Up @@ -43,4 +43,4 @@ include-package-data = true
where = ["src"]

[tool.setuptools.package-data]
py4D_browser = ["*.png"]
py4D_browser = ["*.png"]
24 changes: 22 additions & 2 deletions src/py4D_browser/empad2_reader.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import empad2
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QApplication
import numpy as np


class StatusBarWriter:
def __init__(self, statusBar):
self.statusBar = statusBar
self.app = app = QApplication.instance()

def write(self, message):
self.statusBar.showMessage(message, 1_000)
self.app.processEvents()

def flush(self):
pass


def set_empad2_sensor(self, sensor_name):
self.empad2_calibrations = empad2.load_calibration_data(sensor=sensor_name)
self.statusBar().showMessage(f"{sensor_name} calibrations loaded", 5_000)
Expand Down Expand Up @@ -41,7 +54,14 @@ def load_empad2_dataset(self):

filename = raw_file_dialog(self)
self.datacube = empad2.load_dataset(
filename, self.empad2_background, self.empad2_calibrations
filename,
self.empad2_background,
self.empad2_calibrations,
_tqdm_args={
"desc": "Loading",
"file": StatusBarWriter(self.statusBar()),
"mininterval": 1.0,
},
)

if dummy_data:
Expand Down
27 changes: 26 additions & 1 deletion src/py4D_browser/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class DataViewer(QMainWindow):

from py4D_browser.menu_actions import (
load_file,
load_data_arina,
load_data_auto,
load_data_bin,
load_data_mmap,
Expand All @@ -42,6 +43,7 @@ class DataViewer(QMainWindow):
export_datacube,
export_virtual_image,
show_keyboard_map,
reshape_data,
)

from py4D_browser.update_views import (
Expand Down Expand Up @@ -115,6 +117,14 @@ def setup_menus(self):
self.load_binned_action.triggered.connect(self.load_data_bin)
self.file_menu.addAction(self.load_binned_action)

self.load_arina_action = QAction("Load &Arina Data...", self)
self.load_arina_action.triggered.connect(self.load_data_arina)
self.file_menu.addAction(self.load_arina_action)

self.reshape_data_action = QAction("&Reshape Data...", self)
self.reshape_data_action.triggered.connect(self.reshape_data)
self.file_menu.addAction(self.reshape_data_action)

self.file_menu.addSeparator()

export_label = QAction("Export", self)
Expand Down Expand Up @@ -360,13 +370,22 @@ def setup_menus(self):
img_fft_action = QAction("Virtual Image FFT", self)
img_fft_action.setCheckable(True)
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)

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)
)

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_fft_action.triggered.connect(partial(self.update_real_space_view, False))
img_ewpc_action.triggered.connect(
partial(self.update_diffraction_space_view, False)
)
Expand Down Expand Up @@ -466,6 +485,12 @@ def setup_views(self):
self.fft_widget.getView().setMenuEnabled(False)

# Setup Status Bar
self.realspace_statistics_text = QLabel("Image Stats")
self.diffraction_statistics_text = QLabel("Diffraction Stats")
self.statusBar().addPermanentWidget(VLine())
self.statusBar().addPermanentWidget(self.realspace_statistics_text)
self.statusBar().addPermanentWidget(VLine())
self.statusBar().addPermanentWidget(self.diffraction_statistics_text)
self.statusBar().addPermanentWidget(VLine())
self.statusBar().addPermanentWidget(self.diffraction_space_view_text)
self.statusBar().addPermanentWidget(VLine())
Expand Down
76 changes: 69 additions & 7 deletions src/py4D_browser/menu_actions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from numbers import Real
import py4DSTEM
from PyQt5.QtWidgets import QFileDialog, QMessageBox
import h5py
import os
import numpy as np
import matplotlib.pyplot as plt
from py4D_browser.help_menu import KeyboardMapMenu
from py4D_browser.utils import ResizeDialog
from py4DSTEM.io.filereaders import read_arina


def load_data_auto(self):
Expand All @@ -23,12 +26,48 @@ def load_data_bin(self):
self.load_file(filename, mmap=False, binning=4)


def load_data_arina(self):
filename = self.show_file_dialog()
dataset = read_arina(filename)

# Try to reshape the data to be square
N_patterns = dataset.data.shape[1]
Nxy = np.sqrt(N_patterns)
if np.abs(Nxy - np.round(Nxy)) <= 1e-10:
Nxy = int(Nxy)
dataset.data = dataset.data.reshape(
Nxy, Nxy, dataset.data.shape[2], dataset.data.shape[3]
)
else:
self.statusBar().showMessage(
f"The scan appears to not be square! Found {N_patterns} patterns", 5_000
)

self.datacube = dataset
self.diffraction_scale_bar.pixel_size = self.datacube.calibration.get_Q_pixel_size()
self.diffraction_scale_bar.units = self.datacube.calibration.get_Q_pixel_units()

self.real_space_scale_bar.pixel_size = self.datacube.calibration.get_R_pixel_size()
self.real_space_scale_bar.units = self.datacube.calibration.get_R_pixel_units()

self.fft_scale_bar.pixel_size = (
1.0 / self.datacube.calibration.get_R_pixel_size() / self.datacube.R_Ny
)
self.fft_scale_bar.units = f"1/{self.datacube.calibration.get_R_pixel_units()}"

self.update_diffraction_space_view(reset=True)
self.update_real_space_view(reset=True)

self.setWindowTitle(filename)


def load_file(self, filepath, mmap=False, binning=1):
print(f"Loading file {filepath}")
extension = os.path.splitext(filepath)[-1].lower()
print(f"Type: {extension}")
if extension in (".h5", ".hdf5", ".py4dstem", ".emd"):
datacubes = get_4D(h5py.File(filepath, "r"))
if extension in (".h5", ".hdf5", ".py4dstem", ".emd", ".mat"):
file = h5py.File(filepath, "r")
datacubes = get_ND(file)
print(f"Found {len(datacubes)} 4D datasets inside the HDF5 file...")
if len(datacubes) >= 1:
# Read the first datacube in the HDF5 file into RAM
Expand All @@ -45,7 +84,17 @@ def load_file(self, filepath, mmap=False, binning=1):
self.datacube.calibration.set_Q_pixel_units(Q_units)

else:
raise ValueError("No 4D data detected in the H5 file!")
# if no 4D data was found, look for 3D data
datacubes = get_ND(file, N=3)
print(f"Found {len(datacubes)} 3D datasets inside the HDF5 file...")
if len(datacubes) >= 1:
array = datacubes[0] if mmap else datacubes[0][()]
new_shape = ResizeDialog.get_new_size([1, array.shape[0]], parent=self)
self.datacube = py4DSTEM.DataCube(
array.reshape(*new_shape, *array.shape[1:])
)
else:
raise ValueError("No 4D (or even 3D) data detected in the H5 file!")
elif extension in [".npy"]:
self.datacube = py4DSTEM.DataCube(np.load(filepath))
else:
Expand All @@ -72,6 +121,18 @@ def load_file(self, filepath, mmap=False, binning=1):
self.setWindowTitle(filepath)


def reshape_data(self):
new_shape = ResizeDialog.get_new_size(self.datacube.shape[:2], parent=self)
self.datacube.data = self.datacube.data.reshape(
*new_shape, *self.datacube.data.shape[2:]
)

print(f"Reshaping data to {new_shape}")

self.update_diffraction_space_view(reset=True)
self.update_real_space_view(reset=True)


def export_datacube(self, save_format: str):
assert save_format in [
"Raw float32",
Expand Down Expand Up @@ -156,7 +217,7 @@ def show_file_dialog(self) -> str:
self,
"Open 4D-STEM Data",
"",
"4D-STEM Data (*.dm3 *.dm4 *.raw *.mib *.gtg *.h5 *.hdf5 *.emd *.py4dstem *.npy *.npz);;Any file (*)",
"4D-STEM Data (*.dm3 *.dm4 *.raw *.mib *.gtg *.h5 *.hdf5 *.emd *.py4dstem *.npy *.npz *.mat);;Any file (*)",
)
if filename is not None and len(filename[0]) > 0:
return filename[0]
Expand Down Expand Up @@ -207,16 +268,17 @@ def get_savefile_name(self, file_format) -> str:
raise ValueError("Could get save file")


def get_4D(f, datacubes=None):
def get_ND(f, datacubes=None, N=4):
# Traverse an h5py.File and look for Datasets with N dimensions
if datacubes is None:
datacubes = []
for k in f.keys():
if isinstance(f[k], h5py.Dataset):
# we found data
if len(f[k].shape) == 4:
if len(f[k].shape) == N:
datacubes.append(f[k])
elif isinstance(f[k], h5py.Group):
get_4D(f[k], datacubes)
get_ND(f[k], datacubes)
return datacubes


Expand Down
Loading
Loading