From d516f2044c1c4568f8ecb57217c95d73924cf47e Mon Sep 17 00:00:00 2001 From: Graeme Winter Date: Tue, 8 Aug 2023 14:09:39 +0100 Subject: [PATCH 1/4] Fix bit depth for Eiger / NXmx for i19-2 (#652) Fix bit depth for Eiger / NXmx for i19-2 Add bit depth check from image metadata as implemented on other Diamond NXmx installations. Fixes dials/dials#2473 --- newsfragments/652.bugfix | 1 + src/dxtbx/format/FormatNXmxDLSI19_2.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 newsfragments/652.bugfix diff --git a/newsfragments/652.bugfix b/newsfragments/652.bugfix new file mode 100644 index 000000000..ec7635b59 --- /dev/null +++ b/newsfragments/652.bugfix @@ -0,0 +1 @@ +``dxtbx``: add fix for Eiger / NXmx data from i19-2 to correctly assign the image bit depth diff --git a/src/dxtbx/format/FormatNXmxDLSI19_2.py b/src/dxtbx/format/FormatNXmxDLSI19_2.py index 6d7f0c7ad..255330eb4 100644 --- a/src/dxtbx/format/FormatNXmxDLSI19_2.py +++ b/src/dxtbx/format/FormatNXmxDLSI19_2.py @@ -8,7 +8,7 @@ from libtbx import Auto from dxtbx.format.FormatNXmx import FormatNXmx -from dxtbx.format.FormatNXmxDLS import FormatNXmxDLS +from dxtbx.format.FormatNXmxDLS import FormatNXmxDLS, get_bit_depth_from_meta from dxtbx.masking import GoniometerMaskerFactory @@ -41,6 +41,11 @@ def __init__(self, image_file, **kwargs): """Initialise the image structure from the given file.""" self._dynamic_shadowing = self.has_dynamic_shadowing(**kwargs) super().__init__(image_file, **kwargs) + try: + self._bit_depth_readout = get_bit_depth_from_meta(self._meta) + except Exception: + self._bit_depth_readout = 16 + def get_goniometer_shadow_masker(self, goniometer=None): """Apply the dynamic mask for a diamond anvil cell with a 76° aperture.""" From 39c49235e26a09779ffd0c5f5bef7c64e6d5d3d3 Mon Sep 17 00:00:00 2001 From: Graeme Winter Date: Thu, 10 Aug 2023 16:32:35 +0100 Subject: [PATCH 2/4] ESRF ID23-2 support (#651) --- newsfragments/651.feature | 1 + .../FormatNXmxEigerFilewriterESRFID232.py | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 newsfragments/651.feature create mode 100644 src/dxtbx/format/FormatNXmxEigerFilewriterESRFID232.py diff --git a/newsfragments/651.feature b/newsfragments/651.feature new file mode 100644 index 000000000..2f790c707 --- /dev/null +++ b/newsfragments/651.feature @@ -0,0 +1 @@ +Add support for Eiger 9M on ESRF ID23-2, which has an undeclared vertical goniometer. diff --git a/src/dxtbx/format/FormatNXmxEigerFilewriterESRFID232.py b/src/dxtbx/format/FormatNXmxEigerFilewriterESRFID232.py new file mode 100644 index 000000000..58d2fcb60 --- /dev/null +++ b/src/dxtbx/format/FormatNXmxEigerFilewriterESRFID232.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import re + +import h5py + +from dxtbx.format.FormatNXmxEigerFilewriter import FormatNXmxEigerFilewriter + +DATA_FILE_RE = re.compile(r"data_\d{6}") + + +class FormatNXmxEigerFilewriterESRFID232(FormatNXmxEigerFilewriter): + _cached_file_handle = None + + @staticmethod + def understand(image_file): + with h5py.File(image_file) as handle: + if "/entry/instrument/detector/detector_number" in handle: + if ( + handle["/entry/instrument/detector/detector_number"][()] + == b"E-18-0133" + ): + return True + return False + + def __init__(self, image_file, **kwargs): + """Initialise the image structure from the given file.""" + super().__init__(image_file, **kwargs) + + def _start(self): + super()._start() + + def _goniometer(self): + return self._goniometer_factory.known_axis((0, 1, 0)) From 206c7cb2c1549db95350f8074a7e087394c596fd Mon Sep 17 00:00:00 2001 From: David Waterman Date: Thu, 10 Aug 2023 16:33:10 +0100 Subject: [PATCH 3/4] FormatCBFMiniEigerChessID7B2 (#649) --- newsfragments/649.feature | 1 + .../format/FormatCBFMiniEigerChessID7B2.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 newsfragments/649.feature create mode 100644 src/dxtbx/format/FormatCBFMiniEigerChessID7B2.py diff --git a/newsfragments/649.feature b/newsfragments/649.feature new file mode 100644 index 000000000..eb736b9a4 --- /dev/null +++ b/newsfragments/649.feature @@ -0,0 +1 @@ +Add CBFMini support for the EIGER2 16M detector at CHESS beamline ID7B2, which has an inverted rotation axis. diff --git a/src/dxtbx/format/FormatCBFMiniEigerChessID7B2.py b/src/dxtbx/format/FormatCBFMiniEigerChessID7B2.py new file mode 100644 index 000000000..9bcee5b52 --- /dev/null +++ b/src/dxtbx/format/FormatCBFMiniEigerChessID7B2.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import sys + +from dxtbx.format.FormatCBFMiniEiger import FormatCBFMiniEiger + + +class FormatCBFMiniEigerChessID7B2(FormatCBFMiniEiger): + """A class for reading mini CBF format Eiger16M images for S/N E-32-0123 + installed at CHESS ID7B2, which has an inverted goniometer axis.""" + + @staticmethod + def understand(image_file): + """Check to see if this looks like an Eiger mini CBF format image, + i.e. we can make sense of it.""" + + header = FormatCBFMiniEiger.get_cbf_header(image_file) + for record in header.split("\n"): + if "# Detector: Dectris EIGER2 Si 16M, S/N E-32-0123" in record: + return True + + return False + + def _goniometer(self): + return self._goniometer_factory.known_axis((-1, 0, 0)) + + +if __name__ == "__main__": + for arg in sys.argv[1:]: + print(FormatCBFMiniEigerChessID7B2.understand(arg)) From 3bc6e67337a0935496e99edb4e6eee25deff9c64 Mon Sep 17 00:00:00 2001 From: "Aaron S. Brewster" Date: Fri, 11 Aug 2023 02:05:16 -0700 Subject: [PATCH 4/4] NXmx: handle multidimensional arrays (#612) Data in NeXus can be 3 or 4 dimensional. 3D: Nimages by slow by fast 4D: Nimages by Nmodules by slow fast Slice image_size and reshape the raw_data in these cases. Co-authored-by: Richard Gildea --- newsfragments/612.bugfix | 1 + src/dxtbx/nexus/__init__.py | 28 ++++--- tests/nexus/test_build_dxtbx_models.py | 103 +++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 newsfragments/612.bugfix diff --git a/newsfragments/612.bugfix b/newsfragments/612.bugfix new file mode 100644 index 000000000..6f4e30c86 --- /dev/null +++ b/newsfragments/612.bugfix @@ -0,0 +1 @@ +NXmx files with multidimensional arrays (images, modules, or both) are now handled. diff --git a/src/dxtbx/nexus/__init__.py b/src/dxtbx/nexus/__init__.py index ccd84a7ec..6fc0d20c6 100644 --- a/src/dxtbx/nexus/__init__.py +++ b/src/dxtbx/nexus/__init__.py @@ -2,7 +2,7 @@ import itertools import logging -from typing import Literal, Optional, Tuple, cast +from typing import Literal, Optional import h5py import numpy as np @@ -404,9 +404,12 @@ def equipment_component_key(dependency): origin -= nxdetector.beam_center_y.magnitude * pixel_size[1] * slow_axis # dxtbx requires image size in the order fast, slow - which is the reverse of what - # is stored in module.data_size - image_size = cast(Tuple[int, int], tuple(map(int, module.data_size[::-1]))) - assert len(image_size) == 2 + # is stored in module.data_size. Additionally, data_size can have more than 2 + # dimensions, for multi-module detectors. So take the last two dimensions and reverse + # them. Examples: + # [1,2,3] --> (3, 2) + # [1,2] --> (2, 1) + image_size = (int(module.data_size[-1]), int(module.data_size[-2])) underload = ( float(nxdetector.underload_value) if nxdetector.underload_value is not None @@ -475,13 +478,17 @@ def get_static_mask(nxdetector: nxmx.NXdetector) -> tuple[flex.bool, ...] | None pixel_mask = nxdetector.pixel_mask except KeyError: return None - if pixel_mask is None or not pixel_mask.size or pixel_mask.ndim != 2: + if pixel_mask is None or not pixel_mask.size: return None all_slices = get_detector_module_slices(nxdetector) - return tuple( - flumpy.from_numpy(np.ascontiguousarray(pixel_mask[slices])) == 0 - for slices in all_slices - ) + all_mask_slices = [] + for slices in all_slices: + mask_slice = flumpy.from_numpy(np.ascontiguousarray(pixel_mask[slices])) == 0 + mask_slice.reshape( + flex.grid(mask_slice.all()[-2:]) + ) # handle 3 or 4 dimension arrays + all_mask_slices.append(mask_slice) + return tuple(all_mask_slices) def _dataset_as_flex( @@ -562,5 +569,8 @@ def get_raw_data( data_as_flex = _dataset_as_flex( sliced_outer, tuple(module_slices), bit_depth=bit_depth ) + data_as_flex.reshape( + flex.grid(data_as_flex.all()[-2:]) + ) # handle 3 or 4 dimension arrays all_data.append(data_as_flex) return tuple(all_data) diff --git a/tests/nexus/test_build_dxtbx_models.py b/tests/nexus/test_build_dxtbx_models.py index 8b3b684ac..82c33ca86 100644 --- a/tests/nexus/test_build_dxtbx_models.py +++ b/tests/nexus/test_build_dxtbx_models.py @@ -306,6 +306,109 @@ def test_get_dxtbx_detector_beam_center_fallback(nxmx_example): ) +@pytest.fixture +def detector_with_multiple_modules(): + + with h5py.File(" ", "w", **pytest.h5_in_memory) as f: + + detector = f.create_group("/entry/instrument/detector") + detector.attrs["NX_class"] = "NXdetector" + detector["beam_center_x"] = 2079.79727597266 + detector["beam_center_y"] = 2225.38773853771 + detector["count_time"] = 0.00285260857097799 + detector["depends_on"] = "/entry/instrument/detector/transformations/det_z" + detector["description"] = "Eiger 16M" + detector["distance"] = 0.237015940260233 + detector.create_dataset("data", data=np.zeros((100, 100))) + detector["sensor_material"] = "Silicon" + detector["sensor_thickness"] = 0.00045 + detector["sensor_thickness"].attrs["units"] = b"m" + detector["x_pixel_size"] = 7.5e-05 + detector["y_pixel_size"] = 7.5e-05 + detector["underload_value"] = 0 + detector["saturation_value"] = 9266 + detector["frame_time"] = 0.1 + detector["frame_time"].attrs["units"] = "s" + detector["bit_depth_readout"] = np.array(32) + mask = np.zeros((2, 100, 200), dtype="i8") + detector.create_dataset("pixel_mask", data=mask) + + detector_transformations = detector.create_group("transformations") + detector_transformations.attrs["NX_class"] = "NXtransformations" + det_z = detector_transformations.create_dataset("det_z", data=np.array([289.3])) + det_z.attrs["depends_on"] = b"." + det_z.attrs["transformation_type"] = b"translation" + det_z.attrs["units"] = b"mm" + det_z.attrs["vector"] = np.array([0.0, 0.0, 1.0]) + + def make_module(name, depends_on, data_origin, fast_direction, slow_direction): + module = detector.create_group(name) + module.attrs["NX_class"] = "NXdetector_module" + module.create_dataset("data_size", data=np.array([1, 100, 200])) + module.create_dataset("data_origin", data=np.array(data_origin)) + fast = module.create_dataset("fast_pixel_direction", data=0.075) + fast.attrs["transformation_type"] = "translation" + fast.attrs["depends_on"] = depends_on + fast.attrs["vector"] = np.array(fast_direction) + fast.attrs["units"] = "mm" + slow = module.create_dataset("slow_pixel_direction", data=0.075) + slow.attrs["transformation_type"] = "translation" + slow.attrs["depends_on"] = depends_on + slow.attrs["vector"] = np.array(slow_direction) + slow.attrs["units"] = "mm" + + make_module( + name="m0", + depends_on="/entry/instrument/detector/transformations/det_z", + data_origin=[0, 0, 0], + fast_direction=[-0.999998, -0.001781, 0], + slow_direction=[-0.001781, 0.999998, 0], + ) + make_module( + name="m1", + depends_on="/entry/instrument/detector/transformations/det_z", + data_origin=[1, 0, 0], + fast_direction=[-0.999998, -0.001781, 0], + slow_direction=[-0.001781, 0.999998, 0], + ) + + nxdata = f.create_group("/entry/data") + nxdata.attrs["NX_class"] = "NXdata" + nxdata.create_dataset( + "data", + data=np.array( + [np.full((2, 100, 200), i, dtype=np.int32) for i in range(3)] + ), + ) + nxdata.attrs["signal"] = "/entry/data/data" + + yield f + + +def test_get_dxtbx_detector_with_multiple_modules(detector_with_multiple_modules): + det = nxmx.NXdetector(detector_with_multiple_modules["/entry/instrument/detector"]) + wavelength = 1 + + detector = dxtbx.nexus.get_dxtbx_detector(det, wavelength) + assert len(detector) == 2 + for panel in detector: + assert panel.get_image_size() == (200, 100) + + nxdata = nxmx.NXdata(detector_with_multiple_modules["/entry/data"]) + for i in range(3): + raw_data = dxtbx.nexus.get_raw_data(nxdata, det, i) + assert len(raw_data) == 2 + for module_data in raw_data: + assert module_data.all() == (100, 200) + assert module_data.all_eq(i) + + mask = dxtbx.nexus.get_static_mask(det) + assert len(mask) == 2 + for module_mask in mask: + assert isinstance(module_mask, flex.bool) + assert module_mask.all() == (100, 200) + + @pytest.fixture def detector_with_two_theta(): with h5py.File(" ", "w", **pytest.h5_in_memory) as f: