diff --git a/pyproject.toml b/pyproject.toml index 365ce82..e8c64fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta" [project] name = "py4D_browser" -version = "1.5.0" +version = "1.5.1" authors = [ - { name="Steven Zeltmann", email="steven.zeltmann@lbl.gov" }, + { name="Steven Zeltmann", email="steven.zeltmann@berkeley.edu" }, ] description = "A 4D-STEM data browser built on py4DSTEM." readme = "README.md" diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index adb0764..1415959 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -51,7 +51,6 @@ class DataViewer(QMainWindow): show_keyboard_map, reshape_data, set_datacube, - update_scalebars, copy_vimg_to_clipboard, copy_diff_to_clipboard, copy_result_to_clipboard, diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index 2745bfa..5d7db7d 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -39,12 +39,6 @@ def load_data_arina(self: "DataViewer"): self.statusBar().showMessage(f"Arina data was loaded as 3D, please reshape...") 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.update_diffraction_space_view(reset=True) self.update_real_space_view(reset=True) @@ -68,6 +62,13 @@ def load_file(self: "DataViewer", filepath, mmap=False, binning=1): if len(parent) > 1 and "emd_group_type" in file[parent].attrs: print("This appears to be an emdfile... reading natively") self.datacube = py4DSTEM.DataCube.from_h5(datacubes[0].file[parent]) + try: + calibration = py4DSTEM.Calibration.from_h5( + datacubes[0].file["/datacube_root/metadatabundle/calibration"] + ) + self.datacube.calibration = calibration + except Exception as e: + self.statusBar().showMessage(str(e)) else: self.datacube = py4DSTEM.DataCube( datacubes[0] if mmap else datacubes[0][()] @@ -101,8 +102,6 @@ def load_file(self: "DataViewer", filepath, mmap=False, binning=1): binfactor=binning, ) - self.update_scalebars() - self.update_diffraction_space_view(reset=True) self.update_real_space_view(reset=True) @@ -113,8 +112,6 @@ def load_file(self: "DataViewer", filepath, mmap=False, binning=1): def set_datacube(self: "DataViewer", datacube, window_title): self.datacube = datacube - self.update_scalebars() - self.update_diffraction_space_view(reset=True) self.update_real_space_view(reset=True) @@ -122,35 +119,6 @@ def set_datacube(self: "DataViewer", datacube, window_title): self.signal_datacube_changed.emit() -def update_scalebars(self: "DataViewer"): - - realspace_translation = { - "A": "Å", - } - reciprocal_translation = { - "A^-1": "Å⁻¹", - } - - self.diffraction_scale_bar.pixel_size = self.datacube.calibration.get_Q_pixel_size() - q_units = self.datacube.calibration.get_Q_pixel_units() - self.diffraction_scale_bar.units = ( - reciprocal_translation[q_units] - if q_units in reciprocal_translation.keys() - else q_units - ) - - self.real_space_scale_bar.pixel_size = self.datacube.calibration.get_R_pixel_size() - r_units = self.datacube.calibration.get_R_pixel_units() - self.real_space_scale_bar.units = ( - realspace_translation[r_units] - if r_units in realspace_translation.keys() - else r_units - ) - - self.diffraction_scale_bar.updateBar() - self.real_space_scale_bar.updateBar() - - def reshape_data(self: "DataViewer"): new_shape = ResizeDialog.get_new_size(self.datacube.shape[:2], parent=self) self.datacube.data = self.datacube.data.reshape( diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 4bbbec2..e4a990a 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -7,6 +7,7 @@ from PyQt5 import QtCore from PyQt5.QtGui import QCursor import os +from py4D_browser.utils import format_unit from py4D_browser.utils import ( @@ -21,6 +22,7 @@ CircleGeometry, AnnulusGeometry, PointGeometry, + strtobool, ) from typing import TYPE_CHECKING @@ -248,7 +250,12 @@ def update_real_space_view(self: "DataViewer", reset=False): # Debug mode for displaying the mask if "MASK_DEBUG" in os.environ: - self.set_diffraction_image(mask.astype(np.float32), reset=reset) + self.set_diffraction_image( + mask.astype(np.float32), + reset=reset, + pixel_size=self.datacube.calibration.get_Q_pixel_size(), + pixel_units=format_unit(self.datacube.calibration.get_Q_pixel_units()), + ) return mask = mask.astype(np.float32) @@ -309,12 +316,25 @@ def update_real_space_view(self: "DataViewer", reset=False): else: raise ValueError("Oopsie") - self.set_virtual_image(vimg, reset=reset) + self.set_virtual_image( + vimg, + reset=reset, + pixel_size=self.datacube.calibration.get_R_pixel_size(), + pixel_units=format_unit(self.datacube.calibration.get_R_pixel_units()), + ) -def set_virtual_image(self: "DataViewer", vimg, reset=False): +def set_virtual_image( + self: "DataViewer", vimg, reset=False, pixel_size=None, pixel_units=None +): self.unscaled_realspace_image = vimg self._render_virtual_image(reset=reset) + if pixel_size is not None: + self.real_space_scale_bar.pixel_size = pixel_size + if pixel_units is not None: + self.real_space_scale_bar.units = pixel_units + if pixel_size is not None or pixel_units is not None: + self.real_space_scale_bar.updateBar() self.signal_virtual_image_data_changed.emit() @@ -404,12 +424,25 @@ def update_diffraction_space_view(self: "DataViewer", reset=False): case _: raise ValueError("Unsupported detector shape...") - self.set_diffraction_image(DP, reset=reset) + self.set_diffraction_image( + DP, + reset=reset, + pixel_size=self.datacube.calibration.get_Q_pixel_size(), + pixel_units=format_unit(self.datacube.calibration.get_Q_pixel_units()), + ) -def set_diffraction_image(self: "DataViewer", DP, reset=False): +def set_diffraction_image( + self: "DataViewer", DP, reset=False, pixel_size=None, pixel_units=None +): self.unscaled_diffraction_image = DP self._render_diffraction_image(reset=reset) + if pixel_size is not None: + self.diffraction_scale_bar.pixel_size = pixel_size + if pixel_units is not None: + self.diffraction_scale_bar.units = pixel_units + if pixel_size is not None or pixel_units is not None: + self.diffraction_scale_bar.updateBar() self.signal_diffraction_data_changed.emit() @@ -487,7 +520,9 @@ def update_fft_view(self: "DataViewer", mode: Optional[str] = None): pixel_size=( 1.0 / self.datacube.calibration.get_R_pixel_size() / self.datacube.R_Ny ), - pixel_units=f"{self.datacube.calibration.get_R_pixel_units()}⁻¹", + pixel_units=format_unit( + f"{self.datacube.calibration.get_R_pixel_units()}⁻¹" + ), ) self.fft_widget.getImageItem().setRect(0, 0, fft.shape[1], fft.shape[1]) if mode_switch: @@ -509,7 +544,9 @@ def update_fft_view(self: "DataViewer", mode: Optional[str] = None): pixel_size=( 1.0 / self.datacube.calibration.get_R_pixel_size() / self.datacube.R_Ny ), - pixel_units=f"{self.datacube.calibration.get_R_pixel_units()}⁻¹", + pixel_units=format_unit( + f"{self.datacube.calibration.get_R_pixel_units()}⁻¹" + ), ) self.fft_widget.getImageItem().setRect(0, 0, fft.shape[1], fft.shape[1]) if mode_switch: @@ -527,7 +564,9 @@ def update_fft_view(self: "DataViewer", mode: Optional[str] = None): pixel_size=( 1.0 / self.datacube.calibration.get_Q_pixel_size() / self.datacube.Q_Ny ), - pixel_units=f"{self.datacube.calibration.get_Q_pixel_units()}⁻¹", + pixel_units=format_unit( + f"{self.datacube.calibration.get_Q_pixel_units()}⁻¹" + ), ) else: raise RuntimeError( @@ -609,11 +648,11 @@ def update_realspace_detector(self: "DataViewer"): hover_pen = {"color": "c", "width": 6} hover_handle = {"color": "c", "width": 9} - if self.datacube is None: + if self.unscaled_realspace_image is None: x0, y0 = 0, 0 xr, yr = 4, 4 else: - x, y = self.datacube.data.shape[2:] + x, y = self.unscaled_realspace_image.shape[:2] y0, x0 = x // 2, y // 2 xr, yr = (np.minimum(x, y) / 10,) * 2 @@ -669,11 +708,11 @@ def update_diffraction_detector(self: "DataViewer"): hover_pen = {"color": "c", "width": 6} hover_handle = {"color": "c", "width": 9} - if self.datacube is None: + if self.unscaled_diffraction_image is None: x0, y0 = 0, 0 xr, yr = 4, 4 else: - x, y = self.datacube.data.shape[2:] + x, y = self.unscaled_diffraction_image.shape[:2] y0, x0 = x // 2, y // 2 xr, yr = (np.minimum(x, y) / 10,) * 2 @@ -880,11 +919,19 @@ def update_tooltip(self: "DataViewer"): x = int(np.clip(np.floor(pos_in_data.y()), 0, data.shape[0] - 1)) if np.isrealobj(data): - if QtCore.Qt.ControlModifier == modifier_keys and data.dtype in ( - np.uint32, - np.float32, + if ( + QtCore.Qt.ControlModifier == modifier_keys + and strtobool( + self.settings.value("gui/concatenation_tooltip", "0") + ) + and data.itemsize == 4 ): - display_text = f"[{x},{y}]: {data.view(np.uint32)[x,y]:#08X}" + val = data.view(np.uint32)[x, y] + analog = val & 0x3FFF + digital = (val & 0x3FFFC000) >> 14 + gain = (val & 0x80000000) >> 31 + reserved = (val & 0x40000000) >> 30 + display_text = f"[{x},{y}]: {data.view(np.uint32)[x,y]:#041_b} (R{reserved} G{gain} D{digital} A{analog})" else: display_text = f"[{x},{y}]: {data[x,y]:.5g}" else: diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index 3993310..672fc13 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -243,7 +243,7 @@ def complex_to_Lab( return rgb -def strtobool(val): +def strtobool(val: str) -> bool: """Convert a string representation of truth to true (1) or false (0). True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if @@ -251,8 +251,26 @@ def strtobool(val): """ val = val.lower() if val in ("y", "yes", "t", "true", "on", "1"): - return 1 + return True elif val in ("n", "no", "f", "false", "off", "0"): - return 0 + return False else: raise ValueError("invalid truth value %r" % (val,)) + + +def format_unit(raw_unit): + """Translate py4DSTEM ASCII unit strings to Unicode for display on scale bars. + + py4DSTEM calibration objects return ASCII unit strings like 'A' and 'A^-1'. + This function translates them to their Unicode equivalents for nicer display. + """ + if raw_unit == "A": + return "Å" + if raw_unit == "A^-1": + return "Å⁻¹" + if raw_unit == "px^-1": + return "px⁻¹" + # generic fallback: replace "^-1" with the Unicode superscript + if raw_unit.endswith("^-1"): + return raw_unit[:-3] + "⁻¹" + return raw_unit diff --git a/src/py4d_browser_plugin/calibration_plugin/calibration_plugin.py b/src/py4d_browser_plugin/calibration_plugin/calibration_plugin.py index a34e7a1..1a9669e 100644 --- a/src/py4d_browser_plugin/calibration_plugin/calibration_plugin.py +++ b/src/py4d_browser_plugin/calibration_plugin/calibration_plugin.py @@ -125,6 +125,7 @@ def __init__(self, datacube, parent, diffraction_selector_size=None): diff_left_layout.addWidget(QLabel("Selection Radius"), 2, 0, Qt.AlignRight) self.diff_selection_box = QLineEdit() + self.diff_selection_box.setValidator(QDoubleValidator()) diff_left_layout.addWidget(self.diff_selection_box, 2, 1) self.diff_selection_box.setEnabled(self.diffraction_selector_size is not None) @@ -153,6 +154,25 @@ def __init__(self, datacube, parent, diffraction_selector_size=None): self.kV_input = QLineEdit() kV_left_layout.addWidget(self.kV_input, 0, 1) + orientation_box = QGroupBox("Orientation") + layout.addWidget(orientation_box) + orientation_layout = QHBoxLayout() + orientation_box.setLayout(orientation_layout) + orientation_left_layout = QGridLayout() + orientation_layout.addLayout(orientation_left_layout) + orientation_left_layout.addWidget( + QLabel("Scan Rotation [deg]"), 0, 0, Qt.AlignRight + ) + self.rotation_input = QLineEdit() + self.rotation_input.setValidator(QDoubleValidator()) + orientation_left_layout.addWidget(self.rotation_input, 0, 1) + + orientation_left_layout.addWidget( + QLabel("Pattern Transpose"), 1, 0, Qt.AlignRight + ) + self.transpose_checkbox = QCheckBox() + orientation_left_layout.addWidget(self.transpose_checkbox, 1, 1) + button_layout = QHBoxLayout() button_layout.addStretch() cancel_button = QPushButton("Cancel") @@ -163,6 +183,45 @@ def __init__(self, datacube, parent, diffraction_selector_size=None): button_layout.addWidget(done_button) layout.addLayout(button_layout) + ######### POPULATE ######## + + # Real Space + r_pix = self.datacube.calibration.get_R_pixel_size() + r_unit = self.datacube.calibration.get_R_pixel_units() + + self.realspace_pix_box.setText(f"{r_pix:g}") + self.realspace_fov_box.setText(f"{r_pix * self.datacube.R_Ny:g}") + + r_unit_map = {"A": "Å", "nm": "nm"} + self.realspace_unit_box.setCurrentText(r_unit_map.get(r_unit, "Å")) + + # Diffraction + q_pix = self.datacube.calibration.get_Q_pixel_size() + q_unit = self.datacube.calibration.get_Q_pixel_units() + + self.diff_pix_box.setText(f"{q_pix:g}") + self.diff_fov_box.setText(f"{q_pix * self.datacube.Q_Ny:g}") + if self.diffraction_selector_size: + self.diff_selection_box.setText( + f"{q_pix * self.diffraction_selector_size:g}" + ) + + q_unit_map = {"mrad": "mrad", "A^-1": "Å⁻¹", "1/nm": "nm⁻¹"} + self.diff_unit_box.setCurrentText(q_unit_map.get(q_unit, "mrad")) + + # Voltage + try: + voltage = self.datacube.calibration["voltage"] + self.kV_input.setText(f"{voltage / 1e3:g}") + except (KeyError, TypeError): + pass + + # Orientation + rotation = self.datacube.calibration.get_QR_rotation_degrees() + if rotation is not None: + self.rotation_input.setText(f"{rotation:g}") + self.transpose_checkbox.setChecked(self.datacube.calibration.get_QR_flip()) + ######### CALLBACKS ######## self.realspace_pix_box.textEdited.connect(self.realspace_pix_box_changed) self.realspace_fov_box.textEdited.connect(self.realspace_fov_box_changed) @@ -171,6 +230,7 @@ def __init__(self, datacube, parent, diffraction_selector_size=None): self.diff_selection_box.textEdited.connect( self.diffraction_selection_box_changed ) + self.rotation_input.textEdited.connect(self.rotation_input_changed) def realspace_pix_box_changed(self, new_text): pix_size = float(new_text) @@ -206,15 +266,18 @@ def diffraction_fov_box_changed(self, new_text): def diffraction_selection_box_changed(self, new_text): if self.diffraction_selector_size: - sel_size = float(new_text) + try: + sel_size = float(new_text) + except ValueError: + return pix_size = sel_size / self.diffraction_selector_size - fov = pix_size * self.datacube.Q_Nx + fov = pix_size * self.datacube.Q_Ny self.diff_pix_box.setText(f"{pix_size:g}") self.diff_fov_box.setText(f"{fov:g}") - sel_size = pix_size * self.diffraction_selector_size - self.diff_selection_box.setText(f"{sel_size:g}") + def rotation_input_changed(self, new_text): + pass # no dependent fields to update def set_and_close(self): @@ -245,11 +308,34 @@ def set_and_close(self): kV_text = self.kV_input.text() if kV_text != "": kV = float(kV_text) - self.datacube.calibration["voltage"] = kV + self.datacube.calibration["voltage"] = kV * 1e3 # note there is no canonical tag for voltage, so we are # going to make our own key for it - self.parent.update_scalebars() + rotation_text = self.rotation_input.text() + if rotation_text != "": + rotation = float(rotation_text) + self.datacube.calibration.set_QR_rotation(np.deg2rad(rotation)) + + self.datacube.calibration.set_QR_flip(self.transpose_checkbox.isChecked()) + + from py4D_browser.utils import format_unit + + self.parent.real_space_scale_bar.pixel_size = ( + self.datacube.calibration.get_R_pixel_size() + ) + self.parent.real_space_scale_bar.units = format_unit( + self.datacube.calibration.get_R_pixel_units() + ) + self.parent.real_space_scale_bar.updateBar() + + self.parent.diffraction_scale_bar.pixel_size = ( + self.datacube.calibration.get_Q_pixel_size() + ) + self.parent.diffraction_scale_bar.units = format_unit( + self.datacube.calibration.get_Q_pixel_units() + ) + self.parent.diffraction_scale_bar.updateBar() print("New calibration") print(self.datacube.calibration) diff --git a/src/py4d_browser_plugin/tcBF_plugin/tcBF_plugin.py b/src/py4d_browser_plugin/tcBF_plugin/tcBF_plugin.py index 9a1c876..82ceecd 100644 --- a/src/py4d_browser_plugin/tcBF_plugin/tcBF_plugin.py +++ b/src/py4d_browser_plugin/tcBF_plugin/tcBF_plugin.py @@ -84,7 +84,12 @@ def launch_auto(self): plot_convergence=False, ) - parent.set_virtual_image(tcBF.recon_BF, reset=True) + parent.set_virtual_image( + tcBF.recon_BF, + reset=True, + pixel_size=parent.datacube.calibration.get_R_pixel_size(), + pixel_units=parent.datacube.calibration.get_R_pixel_units(), + ) class ManualTCBFDialog(QDialog): @@ -235,4 +240,9 @@ def reconstruct(self): if pad: reconstruction = reconstruction[pad_width:-pad_width, pad_width:-pad_width] - self.parent.set_virtual_image(reconstruction, reset=True) + self.parent.set_virtual_image( + reconstruction, + reset=True, + pixel_size=self.parent.datacube.calibration.get_R_pixel_size(), + pixel_units=self.parent.datacube.calibration.get_R_pixel_units(), + )