Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e65cd56
Add core, components, webgpu, fft - javascript
bobleesj Jan 12, 2026
ec63195
add python source code for show4dstem
bobleesj Jan 12, 2026
d2de496
add init, array, show4dstem files
bobleesj Jan 12, 2026
027ccd0
Merge upstream/dev into quantem.widget-show4dstem
bobleesj Jan 12, 2026
b19f4f0
clean-up dead trailets, and support A/mmrad now
bobleesj Jan 12, 2026
5874d89
remove extra fft overlay
bobleesj Jan 12, 2026
3b88c63
pre-commute BF/ABF, etc. for instant preset switching
bobleesj Jan 12, 2026
f2a17c4
reset all when user clicks
bobleesj Jan 13, 2026
f4f085b
add VI ROI, stats display, scale modes, and UI improvements
bobleesj Jan 20, 2026
6c2fa20
update config for theme inheritance, remove stale tests
bobleesj Jan 20, 2026
ac8fe47
ignore Playwright testing artifacts
bobleesj Jan 20, 2026
673bcf1
make Reset/Export buttons more compact
bobleesj Jan 20, 2026
3e25bd1
vertically center control rows in containers
bobleesj Jan 20, 2026
6f8905d
add bordered style to each control row
bobleesj Jan 20, 2026
90bce86
make control rows fit-content, increase histogram size
bobleesj Jan 20, 2026
ad0237b
fix VI histogram to update when scale mode changes
bobleesj Jan 20, 2026
b178b88
move scale/colormap transforms to JS for instant feedback
bobleesj Jan 20, 2026
ec2d227
add dynamic theme detection for light/dark mode support
bobleesj Jan 20, 2026
60631bf
inline all dependencies into show4dstem.tsx for self-contained widget
bobleesj Jan 20, 2026
75a29d4
remove unused shared code after inlining into show4dstem
bobleesj Jan 20, 2026
46f3718
fix rectangular scan aspect ratio in VI and FFT panels
bobleesj Jan 20, 2026
08eef7b
restructure js/ into per-widget folders
bobleesj Jan 20, 2026
c7d8696
Merge pull request #3 from bobleesj/quantem.widget-show4dstem-ophus-v1
bobleesj Jan 20, 2026
edf4c97
george feedback - 3rd FFT column, hot pixel, loading speed, reset but…
bobleesj Jan 22, 2026
fa41d27
remove deadcode
bobleesj Jan 22, 2026
1b20a94
Merge pull request #4 from bobleesj/quantem.widget-show4dstem-george-…
bobleesj Jan 22, 2026
2039076
cache detector coordinate for VI movement for higher FPS
bobleesj Jan 25, 2026
a55b6ec
faster FPS, do not render twice when ROI changes
bobleesj Jan 25, 2026
9436c74
remove unused code, batch model updates for faster ROI dragging
bobleesj Jan 26, 2026
25a59b4
restore test_widget_show4dstem.py, remove visual_check.py
bobleesj Jan 26, 2026
b207e87
Merge pull request #5 from bobleesj/quantem.widget-show4dstem-george-…
bobleesj Jan 26, 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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,9 @@ ipynb-playground/
# widget (JS build artifacts)
node_modules/
widget/src/quantem/widget/static/

# Playwright testing
playwright-report/
playwright/
test-results/
playwright*.config.ts
118 changes: 92 additions & 26 deletions src/quantem/core/io/file_readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,41 @@ def read_2d(
return dataset


def _find_4d_dataset(group: h5py.Group, path: list[str] | None = None) -> tuple[list[str], h5py.Dataset] | None:
"""Recursively search for a 4D dataset in an HDF5 group."""
if path is None:
path = []
for key in group.keys():
item = group[key]
current_path = path + [key]
if isinstance(item, h5py.Dataset):
if item.ndim == 4:
return current_path, item
elif isinstance(item, h5py.Group):
result = _find_4d_dataset(item, current_path)
if result is not None:
return result
return None


def _find_calibration(group: h5py.Group, path: list[str] | None = None) -> tuple[list[str], h5py.Group] | None:
"""Recursively search for a calibration group containing R_pixel_size and Q_pixel_size."""
if path is None:
path = []
for key in group.keys():
item = group[key]
current_path = path + [key]
if isinstance(item, h5py.Group):
# Check if this group has calibration keys
if "R_pixel_size" in item and "Q_pixel_size" in item:
return current_path, item
# Recurse into subgroups
result = _find_calibration(item, current_path)
if result is not None:
return result
return None


def read_emdfile_to_4dstem(
file_path: str | PathLike,
data_keys: list[str] | None = None,
Expand All @@ -146,42 +181,73 @@ def read_emdfile_to_4dstem(
"""
File reader for legacy `emdFile` / `py4DSTEM` files.

If data_keys and calibration_keys are not provided, the function will
automatically search for a 4D dataset and calibration metadata.

Parameters
----------
file_path: str | PathLike
Path to data
data_keys: list[str], optional
List of keys to navigate to the data. If None, auto-detects.
calibration_keys: list[str], optional
List of keys to navigate to calibration. If None, auto-detects.

Returns
--------
Dataset4dstem
"""
with h5py.File(file_path, "r") as file:
# Access the data directly
data_keys = ["datacube_root", "datacube", "data"] if data_keys is None else data_keys
print("keys: ", data_keys)
try:
data = file
for key in data_keys:
data = data[key] # type: ignore
except KeyError:
raise KeyError(f"Could not find key {data_keys} in {file_path}")

# Access calibration values directly
calibration_keys = (
["datacube_root", "metadatabundle", "calibration"]
if calibration_keys is None
else calibration_keys
)
try:
calibration = file
for key in calibration_keys:
calibration = calibration[key] # type: ignore
except KeyError:
raise KeyError(f"Could not find calibration key {calibration_keys} in {file_path}")
r_pixel_size = calibration["R_pixel_size"][()] # type: ignore
q_pixel_size = calibration["Q_pixel_size"][()] # type: ignore
r_pixel_units = calibration["R_pixel_units"][()] # type: ignore
q_pixel_units = calibration["Q_pixel_units"][()] # type: ignore
# Auto-detect or use provided data keys
if data_keys is None:
result = _find_4d_dataset(file)
if result is None:
raise KeyError(f"Could not find any 4D dataset in {file_path}")
data_keys, data = result
else:
try:
data = file
for key in data_keys:
data = data[key] # type: ignore
except KeyError:
raise KeyError(f"Could not find key {data_keys} in {file_path}")

# Auto-detect or use provided calibration keys
if calibration_keys is None:
result = _find_calibration(file)
if result is None:
# No calibration found, use defaults
r_pixel_size = 1.0
q_pixel_size = 1.0
r_pixel_units = "pixels"
q_pixel_units = "pixels"
else:
calibration_keys, calibration = result
r_pixel_size = calibration["R_pixel_size"][()] # type: ignore
q_pixel_size = calibration["Q_pixel_size"][()] # type: ignore
r_pixel_units = calibration.get("R_pixel_units", [()])
if hasattr(r_pixel_units, "__getitem__"):
r_pixel_units = r_pixel_units[()]
q_pixel_units = calibration.get("Q_pixel_units", [()])
if hasattr(q_pixel_units, "__getitem__"):
q_pixel_units = q_pixel_units[()]
else:
try:
calibration = file
for key in calibration_keys:
calibration = calibration[key] # type: ignore
except KeyError:
raise KeyError(f"Could not find calibration key {calibration_keys} in {file_path}")
r_pixel_size = calibration["R_pixel_size"][()] # type: ignore
q_pixel_size = calibration["Q_pixel_size"][()] # type: ignore
r_pixel_units = calibration["R_pixel_units"][()] # type: ignore
q_pixel_units = calibration["Q_pixel_units"][()] # type: ignore

# Decode bytes to string if needed
if isinstance(r_pixel_units, bytes):
r_pixel_units = r_pixel_units.decode("utf-8")
if isinstance(q_pixel_units, bytes):
q_pixel_units = q_pixel_units.decode("utf-8")

dataset = Dataset4dstem.from_array(
array=data,
Expand Down
4 changes: 4 additions & 0 deletions src/quantem/core/utils/validators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, TypeAlias, Union, overload
Expand All @@ -12,6 +14,7 @@
if TYPE_CHECKING:
import cupy as cp
import torch

TensorLike: TypeAlias = ArrayLike | torch.Tensor
else:
TensorLike: TypeAlias = ArrayLike
Expand All @@ -20,6 +23,7 @@
if config.get("has_cupy"):
import cupy as cp


# --- Dataset Validation Functions ---
def ensure_valid_array(
array: "np.ndarray | cp.ndarray", dtype: DTypeLike = None, ndim: int | None = None
Expand Down
8 changes: 7 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 0 additions & 33 deletions widget/js/index.jsx

This file was deleted.

17 changes: 17 additions & 0 deletions widget/js/show2d/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Placeholder for Show2D widget
// TODO: Implement 2D image viewer widget

import * as React from "react";
import { createRender } from "@anywidget/react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

function Show2D() {
return (
<Box sx={{ p: 2, border: "1px solid #444", borderRadius: 1 }}>
<Typography>Show2D - Coming Soon</Typography>
</Box>
);
}

export const render = createRender(Show2D);
17 changes: 17 additions & 0 deletions widget/js/show3d/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Placeholder for Show3D widget
// TODO: Implement 3D volume viewer widget

import * as React from "react";
import { createRender } from "@anywidget/react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

function Show3D() {
return (
<Box sx={{ p: 2, border: "1px solid #444", borderRadius: 1 }}>
<Typography>Show3D - Coming Soon</Typography>
</Box>
);
}

export const render = createRender(Show3D);
Loading