Source code for cdiutils.io.nanomax

"""
Loader for the Nanomax beamline at MAXIV.
See:
https://www.maxiv.lu.se/beamlines-accelerators/beamlines/nanomax/
"""

import numpy as np

from cdiutils.io.loader import H5TypeLoader, h5_safe_load


[docs] class NanoMAXLoader(H5TypeLoader): """ Data loader for MAX IV NanoMAX beamline. Handles HDF5 files from NanoMAX beamline at MAX IV, supporting Eiger500k detector. NanoMAX has simpler file structure than other beamlines - no separate sample_name or scan number parameters needed. Attributes: angle_names: Mapping from canonical names to NanoMAX motor names: - ``sample_outofplane_angle`` -> ``"gontheta"`` - ``sample_inplane_angle`` -> ``"gonphi"`` - ``detector_outofplane_angle`` -> ``"delta"`` - ``detector_inplane_angle`` -> ``"gamma"`` authorised_detector_names: Tuple of supported detectors: ``("eiger500k",)``. Notes: Unlike other beamlines, NanoMAX stores all data in a single HDF5 file per measurement, eliminating need for sample_name or scan parameters in most methods. See Also: :class:`Loader` for factory method and base class documentation. """ angle_names = { "sample_outofplane_angle": "gontheta", "sample_inplane_angle": "gonphi", "detector_outofplane_angle": "delta", "detector_inplane_angle": "gamma", } authorised_detector_names = ("eiger500k",)
[docs] def __init__( self, experiment_file_path: str, detector_name: str = "eiger500k", flat_field: np.ndarray | str = None, alien_mask: np.ndarray | str = None, **kwargs, ) -> None: """ Initialise NanoMAX data loader. Args: experiment_file_path: Path to HDF5 scan file. Contains all data and metadata for the measurement. detector_name: Detector identifier. Defaults to ``"eiger500k"``. flat_field: Flat-field correction array or path to .npy/.npz file. alien_mask: Bad pixel mask array or path. **kwargs: Additional parameters (reserved for future use). """ super().__init__( experiment_file_path, detector_name=detector_name, flat_field=flat_field, alien_mask=alien_mask, )
@h5_safe_load def load_detector_data( self, roi: tuple[slice] = None, rocking_angle_binning: int = None, binning_method: str = "sum", ) -> np.ndarray: """ Load raw detector frames from NanoMAX HDF5 file. Retrieves 3D detector data array with optional ROI, binning, flat-field correction, and masking. Args: roi: Region of interest as tuple of slices or integers. rocking_angle_binning: Binning factor along rocking curve axis. binning_method: Binning operation (``"sum"``, ``"mean"``, or ``"max"``). Default ``"sum"``. Returns: Preprocessed detector data with shape ``(n_frames//binning, n_y, n_x)``. Raises: KeyError: If detector data path ``/entry/measurement/{detector}/frames`` not found in HDF5 file. """ # Where to find the data. key_path = f"/entry/measurement/{self.detector_name}/frames" roi = self._check_roi(roi) try: if rocking_angle_binning: # we first apply the roi for axis1 and axis2 data = self.h5file[key_path][(slice(None), roi[1], roi[2])] else: data = self.h5file[key_path][roi] except KeyError as exc: raise KeyError( f"key_path is wrong (key_path='{key_path}'). " "Are sample_name, scan number or detector name correct?" ) from exc return self.bin_flat_mask( data, roi, self.flat_field, self.alien_mask, rocking_angle_binning, binning_method, ) @h5_safe_load def load_motor_positions( self, roi: tuple[slice] = None, rocking_angle_binning: int = None, ) -> dict: """ Load diffractometer motor angles from HDF5 file. Retrieves motor positions from post-scan snapshots and scanned rocking curve values, applying same ROI and binning as detector data. Args: roi: ROI tuple. Only first element (rocking curve axis) is used. rocking_angle_binning: Binning factor matching detector binning. Angles are averaged when binned. Returns: dict: Motor angles with canonical keys (see :attr:`angle_names` for NanoMAX-specific mapping). Scanned angle (gonphi or gontheta) is 1D array, others are scalars. """ roi = self._check_roi(roi) roi = roi[0] key_path = "entry/snapshots/post_scan/" angles = {key: None for key in NanoMAXLoader.angle_names} for angle, name in NanoMAXLoader.angle_names.items(): angles[angle] = self.h5file[key_path + name][()] # Take care of the rocking curve angle self.rocking_angle = rocking_angle = None for angle in ("gonphi", "gontheta"): if angle in self.h5file["entry/measurement"].keys(): rocking_angle = angle if rocking_angle_binning: rocking_angle_values = self.h5file["entry/measurement"][ angle ][()] else: rocking_angle_values = self.h5file["entry/measurement"][ angle ][roi] # Find what generic angle (in-plane or out-of-plane) it # corresponds to. for angle, name in NanoMAXLoader.angle_names.items(): if name == rocking_angle: self.rocking_angle = angle if self.rocking_angle is not None: angles[self.rocking_angle] = rocking_angle_values angles[self.rocking_angle] = self.bin_rocking_angle_values( angles[self.rocking_angle] ) if roi and rocking_angle_binning: angles[self.rocking_angle] = angles[self.rocking_angle][roi] return angles @h5_safe_load def load_energy(self) -> float: """ Load and return the energy used during the experiment. Args: scan (int): the scan number of the file to load the energy from. Returns: float: the energy value in keV. """ return self.h5file["entry/snapshots/post_scan/energy"][0]
[docs] def load_det_calib_params(self) -> dict: return None
@h5_safe_load def load_detector_shape(self, scan: int = None) -> tuple: return None