Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9621e26
updates to acom + DDF clustering
cophus Aug 30, 2026
1396e24
Edits to DDF and clustering
maclariz Sep 1, 2026
eedf281
adding bloch wave functions
cophus Sep 8, 2026
c3deefe
Adding dynamical calculations, refinement
cophus Sep 8, 2026
7aabfdc
various fixes
cophus Sep 8, 2026
9f1043a
Adding diffraction widget
cophus Sep 9, 2026
d50fec5
Widget fixes
cophus Sep 9, 2026
ff57d6c
Diffraction widget: CIF loader, faded Kikuchi overlay, continuous rot…
cophus Sep 16, 2026
859ce33
Many fixes
cophus Sep 19, 2026
28b4fd6
More fixes.
cophus Sep 20, 2026
8f318b2
fixes
cophus Sep 23, 2026
eb6a79c
fixes
cophus Sep 23, 2026
e356c31
many fixes
cophus Sep 24, 2026
4f128fd
Many updates including dynamical
cophus Sep 27, 2026
b963666
various fixes
cophus Sep 28, 2026
531f3ac
fixes to vis
cophus Sep 28, 2026
c2b7236
plotting fixes
cophus Sep 28, 2026
2fcdf51
test update
cophus Sep 28, 2026
2850425
one more arg
cophus Oct 1, 2026
92862d4
ACOM updates
cophus Oct 2, 2026
603466e
Merge origin/dev into acom
cophus Oct 2, 2026
727e625
Migrate ACOM code to the torch-backed Vector
cophus Oct 2, 2026
6ccbbcf
Store detected peaks in float32
cophus Oct 2, 2026
00ae26e
adding foil normals and other fixes
cophus Oct 2, 2026
e96a314
initial RMC build
cophus Oct 2, 2026
d18b8a1
more RMC updates
cophus Oct 3, 2026
f424e15
updates
cophus Oct 4, 2026
0888aae
updates to displacements
cophus Oct 4, 2026
8e516bd
Merge PR #286 (maclariz/acom): DDF and clustering updates from Ian Ma…
cophus Oct 4, 2026
61f0c38
DDF: port Ian's virtual aperture and polar functions to Vector
cophus Oct 4, 2026
cdc3579
Updating DDF functions
cophus Oct 4, 2026
48b5ad2
few more DDF updates
cophus Oct 5, 2026
b638dbd
changing layout of the diffraction demo
cophus Oct 5, 2026
68d0ba9
various fixes
cophus Oct 6, 2026
e6f2d7e
test fix
cophus Oct 7, 2026
a6c306d
RMC: shell pair correlations and longer Warren-Cowley range
cophus Oct 7, 2026
56997f5
fit probe circle option
smribet Oct 8, 2026
304b09a
Merge remote-tracking branch 'colin/acom' into acom
smribet Oct 8, 2026
44d8ec8
bug fix
smribet Oct 8, 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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ dependencies = [
"optuna>=4.5.0",
"hdf5plugin>=6.0.0",
"torchinfo>=1.8.0",
"ase>=3.23",
"spglib>=2.5",
"em-database>=0.5",
]

Expand Down
1 change: 1 addition & 0 deletions src/quantem/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from quantem.core import visualization as visualization

from quantem import imaging as imaging
from quantem import diffraction as diffraction
from quantem import spectroscopy as spectroscopy
from quantem import diffractive_imaging as diffractive_imaging

Expand Down
1 change: 1 addition & 0 deletions src/quantem/core/datastructures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from quantem.core.datastructures.vector import Vector as Vector

from quantem.core.datastructures.dataset4dstem import Dataset4dstem as Dataset4dstem
from quantem.core.datastructures.polar4dstem import Polar4dstem as Polar4dstem
from quantem.core.datastructures.dataset4d import Dataset4d as Dataset4d
from quantem.core.datastructures.dataset3d import Dataset3d as Dataset3d
from quantem.core.datastructures.dataset2d import Dataset2d as Dataset2d
Expand Down
191 changes: 184 additions & 7 deletions src/quantem/core/datastructures/dataset4dstem.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from os import PathLike
from typing import Any, Self

import matplotlib.pyplot as plt
Expand All @@ -8,6 +9,10 @@

from quantem.core.datastructures.dataset2d import Dataset2d
from quantem.core.datastructures.dataset4d import Dataset4d
from quantem.core.datastructures.polar4dstem import dataset4dstem_polar_transform
from quantem.core.utils.diffractive_imaging_utils import (
fit_probe_circle as _fit_probe_circle,
)
from quantem.core.utils.validators import ensure_valid_array
from quantem.core.visualization import show_2d
from quantem.core.visualization.visualization_utils import ScalebarConfig
Expand Down Expand Up @@ -72,12 +77,22 @@ def __init__(
signal_units : str, optional
Units for the array values, by default "arb. units"
metadata : dict
"r_to_q_rotation_cw_deg": rotation r to q clockwise in degrees
"ellipticity": 3 parameters (a, b, theta (degrees))
Missing keys below are set to None.

"q_to_r_rotation_ccw_deg" : float
Rotation in degrees that maps detector (q) vectors onto the
scan (r) frame. A detector vector (dr, dc) is first swapped to
(dc, dr) if "q_transpose" is True, then rotated as
dr' = cos(t) dr - sin(t) dc, dc' = sin(t) dr + cos(t) dc.
"q_transpose" : bool
If True, swap the detector row and column axes before the
rotation.
"ellipticity" : tuple
3 parameters (a, b, theta in degrees).
_token : object | None, optional
Token to prevent direct instantiation, by default None
"""
mdata_keys_4dstem = ["r_to_q_rotation_cw_deg", "ellipticity"]
mdata_keys_4dstem = ["q_to_r_rotation_ccw_deg", "q_transpose", "ellipticity"]
for k in mdata_keys_4dstem:
if k not in metadata.keys():
metadata[k] = None
Expand All @@ -97,15 +112,15 @@ def __init__(
self._virtual_detectors = {} # Store detector information for regeneration

@classmethod
def from_file(cls, file_path: str, file_type: str) -> "Dataset4dstem":
def from_file(cls, file_path: str | PathLike, file_type: str | None = None) -> "Dataset4dstem":
"""
Create a new Dataset4dstem from a file.

Parameters
----------
file_path : str
file_path : str | PathLike
Path to the data file
file_type : str
file_type : str | None
The type of file reader needed. See rosettasciio for supported formats
https://hyperspy.org/rosettasciio/supported_formats/index.html

Expand Down Expand Up @@ -274,6 +289,39 @@ def get_dp_mean(self, attach: bool = True) -> Dataset2d:

return dp_mean_dataset

def fit_probe_circle(
self,
array: NDArray | Dataset2d | None = None,
threshold: float | None = None,
show: bool = True,
) -> tuple[float, float, float]:
"""Fit a circle to the probe in a diffraction pattern.

Parameters
----------
array : NDArray | Dataset2d | None, optional
2D diffraction pattern to fit, either as an array or a Dataset2d. If
None, uses this dataset's mean diffraction pattern, computing it if needed.
threshold : float | None, optional
Threshold for binarizing the diffraction pattern. If None, Otsu's method
is used.
show : bool, optional
Whether to display the fitted circle, by default True.

Returns
-------
tuple[float, float, float]
Probe center in diffraction-pattern row and column coordinates
(probe_qy0, probe_qx0), followed by the fitted radius.
"""
if array is None:
dp_mean = (
self._dp_mean if hasattr(self, "_dp_mean") else self.get_dp_mean(attach=False)
)
array = dp_mean.array

return _fit_probe_circle(array, threshold=threshold, show=show)

@property
def dp_max(self) -> Dataset2d:
"""
Expand Down Expand Up @@ -566,14 +614,29 @@ def _create_annular_mask(

return (distance >= r_inner) & (distance <= r_outer)

def show_virtual_images(self, figsize: tuple[int, int] | None = None, **kwargs) -> tuple:
def show_virtual_images(
self,
figsize: tuple[int, int] | None = None,
*,
positions: list[tuple[int, int]] | None = None,
position_color: str = "red",
position_size: float = 60.0,
**kwargs,
) -> tuple:
"""
Display all virtual images stored in the dataset using show_2d.

Parameters
----------
figsize : tuple[int, int] | None, optional
Figure size in inches. If None, automatically calculated based on number of images
positions : list of tuple of int, optional
``(row, col)`` scan positions to mark on every image, e.g. the
positions used to tune Bragg disk detection.
position_color : str, default="red"
Color of the position markers.
position_size : float, default=60.0
Area of the position markers in points squared.
**kwargs
Additional keyword arguments passed to show_2d (e.g., cmap, norm, cbar, etc.)

Expand Down Expand Up @@ -623,8 +686,120 @@ def show_virtual_images(self, figsize: tuple[int, int] | None = None, **kwargs)
kwargs.setdefault("scalebar", [scalebar] + [False] * (len(arrays) - 1))
fig, axs = show_2d(arrays_organized, title=titles_organized, figsize=figsize, **kwargs)

if positions is not None and len(positions) > 0:
pos = np.asarray(positions, dtype=float).reshape(-1, 2)
for ax in np.atleast_1d(np.asarray(axs, dtype=object)).ravel():
ax.scatter(
pos[:, 1],
pos[:, 0],
s=position_size,
facecolors="none",
edgecolors=position_color,
linewidths=1.5,
)

return fig, axs

def show_virtual_detectors(
self,
names: str | list[str] | None = None,
*,
colors: list[str] | None = None,
alpha: float = 0.2,
linewidth: float = 1.5,
legend: bool = True,
**kwargs,
) -> tuple:
"""Show the mean diffraction pattern with the virtual detectors drawn on it.

Every detector attached by :meth:`get_virtual_image` is drawn on a single
mean pattern, so their placement relative to the direct beam and the
diffracted rings can be checked at a glance.

Parameters
----------
names : str or list of str, optional
Detector(s) to draw. ``None`` (default) draws all attached detectors.
colors : list of str, optional
One color per detector; defaults to the matplotlib color cycle.
alpha : float, default=0.2
Opacity of the filled detector area. Set to 0 for outlines only.
linewidth : float, default=1.5
Width of the detector outline.
legend : bool, default=True
If ``True``, label the detectors in a legend.
**kwargs
Passed to :func:`~quantem.core.visualization.show_2d`; ``norm``,
``scalebar`` and ``title`` have diffraction-pattern defaults.

Returns
-------
tuple
``(fig, ax)`` from :func:`~quantem.core.visualization.show_2d`.
"""
if not self._virtual_detectors:
raise ValueError("No virtual detectors attached. Create one with get_virtual_image().")
if names is None:
names = list(self._virtual_detectors)
elif isinstance(names, str):
names = [names]
missing = [n for n in names if n not in self._virtual_detectors]
if missing:
raise ValueError(
f"Virtual detector(s) {missing} not found. "
f"Available detectors: {list(self._virtual_detectors)}"
)

if colors is None:
cycle = plt.rcParams["axes.prop_cycle"].by_key().get("color", ["red"])
colors = [cycle[i % len(cycle)] for i in range(len(names))]

dp_mean = self.dp_mean
kwargs.setdefault("norm", {"power": 0.4, "upper_quantile": 0.999})
kwargs.setdefault("title", "mean DP with virtual detectors")
kwargs.setdefault(
"scalebar", ScalebarConfig(sampling=self.sampling[2], units=self.units[2])
)
fig, ax = show_2d(dp_mean.array, **kwargs)

for name, color in zip(names, colors):
det = self._virtual_detectors[name]
mode, geometry, mask = det["mode"], det["geometry"], det["mask"]
if mask is not None:
ax.contour(mask, levels=[0.5], colors=[color], linewidths=linewidth)
ax.plot([], [], color=color, linewidth=linewidth, label=name)
continue
(cy, cx) = geometry[0]
if mode == "circle":
radius = geometry[1]
ax.add_patch(
Circle((cx, cy), radius, color=color, fill=True, alpha=alpha, label=name)
)
ax.add_patch(
Circle((cx, cy), radius, color=color, fill=False, linewidth=linewidth)
)
elif mode == "annular":
r_inner, r_outer = geometry[1]
ax.add_patch(
Wedge(
(cx, cy),
r_outer,
0,
360,
width=r_outer - r_inner,
color=color,
fill=True,
alpha=alpha,
label=name,
)
)
for r in (r_inner, r_outer):
ax.add_patch(Circle((cx, cy), r, color=color, fill=False, linewidth=linewidth))

if legend:
ax.legend(loc="upper right", framealpha=0.8)
return fig, ax

def regenerate_virtual_images(self) -> None:
"""
Regenerate virtual images from stored detector information.
Expand Down Expand Up @@ -798,3 +973,5 @@ def median_filter_masked_pixels(self, mask: np.ndarray, kernel_width: int = 3):
self.array[:, :, index_x, index_y] = np.median(
self.array[:, :, x_min:x_max, y_min:y_max], axis=(2, 3)
)

polar_transform = dataset4dstem_polar_transform
Loading
Loading