Skip to content

Commit 650e73d

Browse files
committed
Add detector flips & rotation to metadata
1 parent 1b5da0c commit 650e73d

4 files changed

Lines changed: 67 additions & 13 deletions

File tree

phaser/hooks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class LoadEmpadProps(Dataclass):
3333
diff_step: t.Optional[float] = None
3434
kv: t.Optional[float] = None
3535
adu: t.Optional[float] = None
36+
det_flips: t.Optional[t.Tuple[bool, bool, bool]] = None
3637

3738

3839
class RawDataHook(Hook[None, RawData]):

phaser/hooks/io/empad.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ def load_empad(args: None, props: LoadEmpadProps) -> RawData:
2121
if path.suffix.lower() == '.json': # load as metadata
2222
meta = EmpadMetadata.from_json(path)
2323
assert meta.path is not None
24-
2524
path = meta.path / meta.raw_filename
2625

26+
if meta.empad_version is not None and meta.empad_version > 1:
27+
raise ValueError("EMPAD v2 import not currently supported (open a Github issue!)")
28+
2729
voltage = props.kv * 1e3 if props.kv is not None else meta.voltage
2830
diff_step = props.diff_step or meta.diff_step
2931
# [x, y] -> [y, x]
3032
scan_shape = t.cast(t.Tuple[int, int], tuple(reversed(meta.scan_shape)))
3133
adu = props.adu or meta.adu
3234
needs_scale = not meta.is_simulated()
35+
det_flips = props.det_flips or meta.det_flips
3336

3437
probe_hook = {
3538
'type': 'focused',
@@ -43,7 +46,7 @@ def load_empad(args: None, props: LoadEmpadProps) -> RawData:
4346
'shape': scan_shape,
4447
'step_size': tuple(s*1e10 for s in reversed(meta.scan_step)), # m to A
4548
'affine': meta.scan_correction[::-1, ::-1] if meta.scan_correction is not None else None,
46-
'rotation': -meta.scan_rotation,
49+
'rotation': meta.det_rotation - meta.scan_rotation,
4750
}
4851

4952
#TODO: add tilt to metafile
@@ -55,6 +58,7 @@ def load_empad(args: None, props: LoadEmpadProps) -> RawData:
5558
probe_hook = scan_hook = tilt_hook = None
5659
adu = None
5760
needs_scale = False
61+
det_flips = props.det_flips
5862

5963
if voltage is None:
6064
raise ValueError("'kv'/'voltage' must be specified by metadata or passed to 'raw_data'")
@@ -66,7 +70,8 @@ def load_empad(args: None, props: LoadEmpadProps) -> RawData:
6670
if not path.exists():
6771
raise ValueError(f"Couldn't find raw data at path {path}")
6872

69-
patterns = numpy.fft.ifftshift(load_4d(path, scan_shape, memmap=True), axes=(-1, -2))
73+
logging.info(f"Loading with detector flips: {list(map(int, det_flips or (True, False, False)))} [y, x, transpose]")
74+
patterns = numpy.fft.ifftshift(load_4d(path, scan_shape, memmap=True, flips=det_flips), axes=(-1, -2))
7075

7176
if needs_scale:
7277
if adu is None:

phaser/io/empad.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from typing_extensions import Self
1313

1414
from phaser.types import IsVersion
15+
from phaser.utils.image import apply_flips
16+
from phaser.utils.num import to_numpy
1517

1618

1719
def _get_dir(f: pane.io.FileOrPath) -> t.Optional[Path]:
@@ -45,9 +47,20 @@ def __post_init__(self):
4547
version: t.Annotated[str, IsVersion(exactly="2.0")] = "2.0"
4648
"""Metadata version"""
4749

50+
empad_version: t.Optional[int] = None
51+
"""Empad version used. Defaults to v1 if not specified."""
52+
4853
raw_filename: str
4954
"""Raw 4DSTEM data filename, relative to metadata location."""
5055

56+
det_flips: t.Optional[t.Tuple[bool, bool, bool]] = None
57+
"""
58+
Flips to apply to the raw diffraction patterns, (flip_y, flip_x, transpose).
59+
Defaults to `(True, False, False)` (appears to be the most common orientation).
60+
"""
61+
det_rotation: float = 0.0
62+
"""Detector rotation (degrees)."""
63+
5164
orig_path: t.Optional[Path] = None
5265
"""Original path to experimental folder."""
5366

@@ -76,7 +89,7 @@ def __post_init__(self):
7689
diff_step: t.Optional[float] = None
7790
"""Diffraction pixel size (mrad/px)."""
7891

79-
scan_rotation: float
92+
scan_rotation: float = 0.0
8093
"""Scan rotation (degrees)."""
8194
scan_shape: t.Tuple[int, int]
8295
"""Scan shape (x, y)."""
@@ -112,8 +125,8 @@ def is_simulated(self) -> bool:
112125
return self.file_type == "pyMultislicer_metadata"
113126

114127

115-
def load_4d(path: t.Union[str, Path], scan_shape: t.Optional[t.Tuple[int, int]] = None,
116-
memmap: bool = False) -> NDArray[numpy.float32]:
128+
def load_4d(path: t.Union[str, Path], scan_shape: t.Optional[t.Tuple[int, int]] = None, *,
129+
memmap: bool = False, flips: t.Optional[t.Tuple[bool, bool, bool]] = None) -> NDArray[numpy.float32]:
117130
"""
118131
Load a raw EMPAD dataset into memory.
119132
@@ -126,6 +139,8 @@ def load_4d(path: t.Union[str, Path], scan_shape: t.Optional[t.Tuple[int, int]]
126139
- `path`: Path to file to load
127140
- `scan_shape`: Scan shape of dataset. Will be inferred from the filename if not specified.
128141
- `memmap`: If specified, memmap the file as opposed to loading it eagerly.
142+
- `flips`: Flips to apply to the diffraction patterns, `(flip_y, flip_x, transpose)`.
143+
Defaults to `(True, False, False)` (appears to be the most common orientation).
129144
130145
Returns a numpy array (or `numpy.memmap`)
131146
"""
@@ -148,26 +163,28 @@ def load_4d(path: t.Union[str, Path], scan_shape: t.Optional[t.Tuple[int, int]]
148163
if not a.size % (130*128) == 0:
149164
raise ValueError(f"File not divisible by 130x128 (size={a.size}).")
150165
a.shape = (-1, 130, 128)
151-
#a = a[:, :128, :]
152166

153167
if a.shape[0] != n_x * n_y:
154168
raise ValueError(f"Got {a.shape[0]} probes, expected {n_x}x{n_y} = {n_x * n_y}.")
155169
a.shape = (n_y, n_x, *a.shape[1:])
156-
a = a[..., 127::-1, :] # flip reciprocal y space, crop junk rows
157170

158-
return a
171+
a = a[..., :128, :] # crop junk rows
172+
return apply_flips(a, flips or (True, False, False)) # defaults to typical EMPAD orientation
159173

160174

161175
@t.overload
162-
def save_4d(arr: NDArray[numpy.float32], *, path: t.Union[str, Path], folder: None = None, name: None = None):
176+
def save_4d(arr: NDArray[numpy.float32], *, path: t.Union[str, Path], folder: None = None, name: None = None,
177+
flips: t.Optional[t.Tuple[bool, bool, bool]] = None):
163178
...
164179

165180
@t.overload
166-
def save_4d(arr: NDArray[numpy.float32], *, path: None = None, folder: t.Union[str, Path], name: t.Optional[str] = None):
181+
def save_4d(arr: NDArray[numpy.float32], *, path: None = None, folder: t.Union[str, Path], name: t.Optional[str] = None,
182+
flips: t.Optional[t.Tuple[bool, bool, bool]] = None):
167183
...
168184

169185
def save_4d(arr: NDArray[numpy.float32], *, path: t.Union[str, Path, None] = None,
170-
folder: t.Union[str, Path, None] = None, name: t.Optional[str] = None): #):
186+
folder: t.Union[str, Path, None] = None, name: t.Optional[str] = None,
187+
flips: t.Optional[t.Tuple[bool, bool, bool]] = None):
171188
"""
172189
Save a raw EMPAD dataset.
173190
@@ -183,6 +200,8 @@ def save_4d(arr: NDArray[numpy.float32], *, path: t.Union[str, Path, None] = Non
183200
- `folder`: Folder to save dataset inside.
184201
- `name`: When `folder` is specified, format to use to determine filename. Defaults to `"scan_x{x}_y{y}.raw"`.
185202
Will be formatted using the scan shape `{'x': n_x, 'y': n_y}`.
203+
- `flips`: Flips to apply to the diffraction patterns, `(flip_y, flip_x, transpose)`.
204+
Defaults to `(True, False, False)` (appears to be the most common orientation).
186205
"""
187206

188207
try:
@@ -206,7 +225,7 @@ def save_4d(arr: NDArray[numpy.float32], *, path: t.Union[str, Path, None] = Non
206225
out_shape[2] = 130 # dead rows
207226

208227
out = numpy.zeros(out_shape, dtype=numpy.float32)
209-
out[..., 127::-1, :] = arr.astype(numpy.float32)
228+
out[..., 127::-1, :] = apply_flips(to_numpy(arr.astype(numpy.float32)), flips or (True, False, False))
210229

211230
with open(path, 'wb') as f:
212231
out.tofile(f)

phaser/utils/image.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@
1414
NumT = t.TypeVar('NumT', bound=numpy.number)
1515

1616

17+
def apply_flips(
18+
data: NDArray[NumT], flips: t.Tuple[bool, bool, bool] = (False, False, False)
19+
) -> NDArray[NumT]:
20+
"""
21+
Applies flips to `data` along the last two axes.
22+
If specified, transpose is applied last (after X and Y flips).
23+
24+
Parameters:
25+
data: Input data array.
26+
flips: Tuple of three booleans `(flip_y, flip_x, transpose)`.
27+
28+
Returns: `data` with the specified flips applied.
29+
"""
30+
if not any(flips):
31+
return data
32+
33+
xp = get_array_module(data)
34+
35+
if flips[0]:
36+
data = xp.flip(data, axis=-2)
37+
if flips[1]:
38+
data = xp.flip(data, axis=-1)
39+
if flips[2]:
40+
data = xp.moveaxis(data, -2, -1)
41+
42+
return data
43+
44+
1745
@t.overload
1846
def remove_linear_ramp( # pyright: ignore[reportOverlappingOverload]
1947
data: NDArray[NumT], mask: t.Optional[NDArray[numpy.bool_]] = None
@@ -196,6 +224,7 @@ def affine_transform(
196224

197225

198226
__all__ = [
227+
'apply_flips',
199228
'remove_linear_ramp', 'colorize_complex', 'scale_to_integral_type',
200229
'affine_transform', 'to_affine_matrix',
201230
'scale_matrix', 'rotation_matrix', 'translation_matrix',

0 commit comments

Comments
 (0)