Skip to content

Commit d3e48db

Browse files
authored
Merge pull request #126 from pycroscopy/jeol-cm
Towards JEOL.py(TEM)
2 parents 58405af + 3ebf932 commit d3e48db

29 files changed

Lines changed: 401 additions & 345 deletions

README.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,15 @@ See - docs/dev_guide.md
5050

5151
### Core installation (simulation mode)
5252

53-
```bash
54-
pip install --find-links ./stubs -e .
55-
```
56-
57-
or with `uv`:
58-
5953
```bash
6054
uv sync
6155
```
6256

63-
This installs `asyncroscopy` and all core dependencies. AutoScript is not required—the
64-
framework will fall back to simulated acquisition automatically.
57+
This installs `asyncroscopy` and all core dependencies. The vendor wheels are
58+
local, version-pinned, and resolved via `[tool.uv.sources]` in `pyproject.toml`
59+
(AutoScript under `stubs/AutoScript_v_1.17/`, PyJEM under
60+
`stubs/PyJEM_v_1.3.0.3564/`). AutoScript/PyJEM hardware is not required—the
61+
framework falls back to simulated acquisition automatically.
6562

6663
### Hardware installation (Thermo Fisher AutoScript)
6764

asyncroscopy/data/data_writer.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import h5py
1111
import numpy as np
12+
import tifffile
1213

1314
DEFAULT_ACQUISITION_DIR = "outputs/tiled_acquisitions"
1415

@@ -35,6 +36,21 @@ def acquisition_filename(
3536
return directory / f"{acquisition_type}_{detector}_{stamp}.{extension.lower().lstrip('.')}"
3637

3738

39+
def _write_tiff(source, path: Path, attrs: dict | None = None) -> None:
40+
"""Write one image to ``path`` as TIFF.
41+
42+
AutoScript adorned images save themselves natively (embedding their own
43+
metadata); anything else is a raw array (JEOL/twin) written via tifffile,
44+
with ``attrs`` (e.g. JEOL's get_detectorsetting() dict) json-encoded into the
45+
TIFF ImageDescription tag so the provenance survives the format.
46+
"""
47+
if hasattr(source, "save"): # adorned images from AutoScript have a save method
48+
source.save(str(path))
49+
else:
50+
array = source.data if hasattr(source, "data") and not isinstance(source, np.ndarray) else source
51+
tifffile.imwrite(str(path), np.asarray(array), description=json.dumps(attrs) if attrs else None)
52+
53+
3854
def save_acquisition(
3955
device,
4056
data_server,
@@ -44,11 +60,31 @@ def save_acquisition(
4460
dataset_name: str = "image",
4561
dataset_attrs: dict | list[dict] | None = None,
4662
file_attrs: dict | None = None,
63+
output_format: str = ".h5",
4764
) -> str:
48-
"""Save one acquisition to one HDF5 file and return its DATA/Tiled key."""
65+
"""Save one acquisition and return its DATA/Tiled key.
66+
67+
``.h5`` writes all detectors into one stacked file; ``.tiff`` writes one
68+
file per detector sharing a timestamp (TIFF cannot stack), registering each.
69+
"""
70+
if output_format not in (".h5", ".tiff"):
71+
raise ValueError(f"Unsupported output_format {output_format!r}; expected '.h5' or '.tiff'")
4972
detector_list = list(detectors) if isinstance(detectors, (list, tuple)) else [detectors]
5073
data_list = list(data) if isinstance(data, (list, tuple)) else [data]
5174
attrs_list = dataset_attrs if isinstance(dataset_attrs, list) else [dataset_attrs] * len(data_list)
75+
76+
if output_format == ".tiff":
77+
save_dir = data_server.save_path if data_server is not None else DEFAULT_ACQUISITION_DIR
78+
directory = Path(save_dir).expanduser()
79+
directory.mkdir(parents=True, exist_ok=True)
80+
stem = f"{acquisition_type}_{datetime.now().strftime('%Y%m%dT%H%M%S%f')}"
81+
for index, (source, detector) in enumerate(zip(data_list, detector_list)):
82+
path = directory / f"{stem}_{detector}.tiff"
83+
_write_tiff(source, path, attrs_list[index])
84+
if data_server is not None:
85+
data_server.register_path(str(path))
86+
return stem
87+
5288
has_labeled_datasets = len(detector_list) > 1 or len(data_list) > 1
5389
detector_label = "_".join([str(detector) for detector in detector_list])
5490
path = acquisition_filename(device, acquisition_type, detector_label, data_server)
@@ -98,3 +134,6 @@ def save_acquisition_hdf5(path: str | Path, datasets: list[dict], file_attrs: di
98134
if key in dset.attrs:
99135
key = f"{key}_{len(dset.attrs)}"
100136
dset.attrs[key] = elem.text.strip()
137+
elif isinstance(metadata, dict):
138+
for key, value in metadata.items():
139+
dset.attrs[key] = value if isinstance(value, (str, int, float, bool, np.number)) else json.dumps(value)

asyncroscopy/instruments/electron_microscope/auto_script.py

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818

1919
import math
2020
import time
21-
from datetime import datetime
22-
from pathlib import Path
2321

2422
import numpy as np
2523
import tango
@@ -250,63 +248,35 @@ def register_stage(self):
250248
# ------------------------------------------------------------------
251249
# Internal acquisition helpers
252250
# ------------------------------------------------------------------
253-
def _persist(self, adorned, acquisition_type, detector, data_server, dataset_name="image"):
254-
"""Save acquired images in the format requested by the SCAN device.
255-
"""
256-
scan = self._detector_proxies.get("scan")
257-
fmt = scan.output_format if scan is not None else ".h5" # ".h5" default
258-
if fmt == ".h5":
259-
return save_acquisition(self, data_server, acquisition_type, detector, adorned, dataset_name=dataset_name)
260-
if fmt != ".tiff":
261-
raise ValueError(f"Unsupported output_format {fmt!r}; expected '.h5' or '.tiff'")
262-
263-
# .tiff → AutoScript native save, one file per detector sharing one stamp
264-
images = list(adorned) if isinstance(adorned, (list, tuple)) else [adorned]
265-
detectors = list(detector) if isinstance(detector, (list, tuple)) else [detector]
266-
if len(images) != len(detectors):
267-
raise ValueError(f"Got {len(images)} images for {len(detectors)} detector(s) {detectors}")
268-
269-
save_dir = data_server.save_path if data_server is not None else DEFAULT_ACQUISITION_DIR
270-
directory = Path(save_dir).expanduser()
271-
directory.mkdir(parents=True, exist_ok=True)
272-
stamp = datetime.now().strftime("%Y%m%dT%H%M%S%f")
273-
stem = f"{acquisition_type}_{stamp}"
274-
# AutoScript returns images in the requested detector order (assumed; verify on hardware)
275-
for img, det in zip(images, detectors):
276-
path = directory / f"{stem}_{det}.tiff"
277-
img.save(str(path))
278-
if data_server is not None:
279-
data_server.register_path(str(path))
280-
return stem
281-
282251
def _acquire_scanned_image(
283252
self,
284253
imsize: int,
285254
dwell_time: float,
286255
detector_list: list[str] = ["haadf"],
287256
scan_region: list[float] = [0.0, 0.0, 1.0, 1.0],
257+
output_format: str = ".h5",
288258
) -> str:
289259
"""
290-
Call AutoScript scanned image acquisition, save one HDF5 file, and return its DATA/Tiled key.
260+
Call AutoScript scanned image acquisition, save it, and return its DATA/Tiled key.
291261
"""
292262
detector_list = [d.upper() for d in detector_list]
293263
settings = StemAcquisitionSettings(dwell_time=dwell_time, detector_types=detector_list, size=imsize, region=Region(RegionCoordinateSystem.RELATIVE, Rectangle(*scan_region)))
294264
adorned = self._microscope.acquisition.acquire_stem_images_advanced(settings)
295265
if not isinstance(adorned, list):
296266
adorned = [adorned]
297267
data_server = self._detector_proxies.get("data")
298-
return self._persist(adorned, "stem_image", detector_list, data_server)
268+
return save_acquisition(self, data_server, "stem_image", detector_list, adorned, output_format=output_format)
299269

300270

301271
def _acquire_camera_image(self, imsize: int, exposure_time: float, detector: str, readout_area: str) -> str:
302272
"""
303-
Call AutoScript acquisition, save the adorned image, and return its path.
273+
Call AutoScript acquisition, save the adorned image, and return its DATA/Tiled key.
304274
this is the advanced version
305275
"""
306276
settings = CameraAcquisitionSettings(camera_detector=detector, size=imsize, exposure_time=exposure_time, fixed_readout_area=readout_area, frame_combining=1)
307277
adorned = self._microscope.acquisition.acquire_camera_image_advanced(settings)
308278
data_server = self._detector_proxies.get("data")
309-
return self._persist(adorned, "camera_image", str(detector), data_server)
279+
return save_acquisition(self, data_server, "camera_image", str(detector), adorned)
310280

311281
def _acquire_scanned_data_advanced(self, imsize: int, dwell_time: float, detector: str, scan_region: list[float]) -> str:
312282
"""

asyncroscopy/instruments/electron_microscope/digital_twin.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,15 +518,16 @@ def _acquire_scanned_image(
518518
dwell_time: float,
519519
detector_list: list[str] = ["haadf"],
520520
scan_region: list[float] = [0.0, 0.0, 1.0, 1.0],
521+
output_format: str = ".h5",
521522
) -> str:
522-
"""Simulate STEM acquisition, save HDF5 data with metadata, and return its DATA/Tiled key."""
523+
"""Simulate STEM acquisition, save the data with metadata, and return its DATA/Tiled key."""
523524
detector_list = [detector.upper() for detector in detector_list]
524525
data_server = self._detector_proxies.get("data")
525526
images = []
526527
for detector in detector_list:
527528
image = self._render_stem_image(int(imsize), float(dwell_time), [detector])
528529
images.append(image)
529-
return save_acquisition(self, data_server, "stem_image", detector_list, images)
530+
return save_acquisition(self, data_server, "stem_image", detector_list, images, output_format=output_format)
530531

531532
def _simulate_spectrum(self, detector_name: str, exposure_time: float) -> dict[str, float]:
532533
"""Simulate EDS spectrum acquisition at the current beam position weighted by surrounding particles."""

asyncroscopy/instruments/electron_microscope/digital_twin_diffraction.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ def _acquire_scanned_image(
356356
dwell_time: float,
357357
detector_list: list[str] = ['haadf'],
358358
scan_region: list[float] = [0.0, 0.0, 1.0, 1.0],
359+
output_format: str = '.h5',
359360
) -> str:
360361
image_size = int(self.overview_image_size)
361362
detector_list = [detector.upper() for detector in detector_list]
@@ -369,7 +370,7 @@ def _acquire_scanned_image(
369370
'sample_pixel_size_nm': float(FOV_NM / int(self.map_size)),
370371
}
371372
images = [self._render_stem_image(image_size, float(dwell_time), [detector]) for detector in detector_list]
372-
return save_acquisition(self, data_server, 'stem_image', detector_list, images, dataset_attrs=attrs)
373+
return save_acquisition(self, data_server, 'stem_image', detector_list, images, dataset_attrs=attrs, output_format=output_format)
373374

374375
def _set_fov(self, fov) -> None:
375376
self.warn_stream('DigitalTwinDiffraction uses a fixed 500 nm field of view.')

asyncroscopy/instruments/electron_microscope/electron_microscope.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def acquire_spectrum(self, detector_name: str) -> str:
123123
def acquire_scanned_image(self, detector_list: list[str] = ['haadf']) -> str:
124124
"""Acquire an image with scanning detectors and return its DATA/Tiled key."""
125125
scan = self._detector_proxies.get('scan')
126-
return self._acquire_scanned_image(scan.imsize, scan.dwell_time, detector_list, list(scan.scan_region))
126+
return self._acquire_scanned_image(scan.imsize, scan.dwell_time, detector_list, list(scan.scan_region), scan.output_format)
127127

128128
@command(dtype_out=str)
129129
def acquire_scanned_data_advanced(self) -> str:
@@ -245,6 +245,7 @@ def _acquire_scanned_image(
245245
dwell_time: float,
246246
detector_list: list[str] = ['haadf'],
247247
scan_region: list[float] = [0.0, 0.0, 1.0, 1.0],
248+
output_format: str = '.h5',
248249
) -> str:
249250
"""Vendor-specific scanned image acquisition implementation."""
250251
pass

0 commit comments

Comments
 (0)