from __future__ import annotations from typing import TYPE_CHECKING, Any import numpy as np if TYPE_CHECKING: from bec_lib import DeviceManagerBase from file_writer.file_writer import HDF5Storage def get_entry(data: dict, name: str, default=None) -> Any: """ Get an entry from the scan data assuming a ..value structure. Args: data (dict): Scan data name (str): Entry name default (Any, optional): Default value. Defaults to None. """ if isinstance(data.get(name), list) and isinstance(data.get(name)[0], dict): return [sub_data.get(name, {}).get("value", default) for sub_data in data.get(name)] return data.get(name, {}).get(name, {}).get("value", default) def NeXus_format( storage: HDF5Storage, data: dict, file_references: dict, device_manager: DeviceManagerBase, ) -> HDF5Storage: """ Prepare the NeXus file format. Args: storage (HDF5Storage): HDF5 storage. Pseudo hdf5 file container that will be written to disk later. data (dict): scan data file_references (dict): File references. Can be used to add external files to the HDF5 file. The path is given relative to the HDF5 file. device_manager (DeviceManagerBase): Device manager. Can be used to check if devices are available. Returns: HDF5Storage: Updated HDF5 storage """ # /entry entry = storage.create_group("entry") entry.attrs["NX_class"] = "NXentry" entry.attrs["definition"] = "NXsas" entry.attrs["start_time"] = data.get("start_time") entry.attrs["end_time"] = data.get("end_time") entry.attrs["version"] = 1.0 # /entry/collection collection = entry.create_group("collection") collection.attrs["NX_class"] = "NXcollection" bec_collection = collection.create_group("bec") # /entry/control control = entry.create_group("control") control.attrs["NX_class"] = "NXmonitor" control.create_dataset(name="mode", data="monitor") control.create_dataset(name="integral", data=get_entry(data, "bpm4i")) # /entry/data main_data = entry.create_group("data") main_data.attrs["NX_class"] = "NXdata" if "eiger_4" in device_manager.devices: main_data.create_soft_link(name="data", target="/entry/instrument/eiger_4/data") elif "eiger9m" in device_manager.devices: main_data.create_soft_link(name="data", target="/entry/instrument/eiger9m/data") elif "pilatus_2" in device_manager.devices: main_data.create_soft_link(name="data", target="/entry/instrument/pilatus_2/data") # /entry/sample control = entry.create_group("sample") control.attrs["NX_class"] = "NXsample" control.create_dataset(name="name", data=get_entry(data, "samplename")) control.create_dataset(name="description", data=data.get("sample_description")) x_translation = control.create_dataset(name="x_translation", data=get_entry(data, "samx")) x_translation.attrs["units"] = "mm" y_translation = control.create_dataset(name="y_translation", data=get_entry(data, "samy")) y_translation.attrs["units"] = "mm" temperature_log = control.create_dataset(name="temperature_log", data=get_entry(data, "temp")) temperature_log.attrs["units"] = "K" # /entry/instrument instrument = entry.create_group("instrument") instrument.attrs["NX_class"] = "NXinstrument" instrument.create_dataset(name="name", data="cSAXS beamline") source = instrument.create_group("source") source.attrs["NX_class"] = "NXsource" source.create_dataset(name="type", data="Synchrotron X-ray Source") source.create_dataset(name="name", data="Swiss Light Source") source.create_dataset(name="probe", data="x-ray") distance = source.create_dataset( name="distance", data=-33800 - np.asarray(get_entry(data, "samz", 0)) ) distance.attrs["units"] = "mm" sigma_x = source.create_dataset(name="sigma_x", data=0.202) sigma_x.attrs["units"] = "mm" sigma_y = source.create_dataset(name="sigma_y", data=0.018) sigma_y.attrs["units"] = "mm" divergence_x = source.create_dataset(name="divergence_x", data=0.000135) divergence_x.attrs["units"] = "radians" divergence_y = source.create_dataset(name="divergence_y", data=0.000025) divergence_y.attrs["units"] = "radians" current = source.create_dataset(name="current", data=get_entry(data, "curr")) current.attrs["units"] = "mA" insertion_device = instrument.create_group("insertion_device") insertion_device.attrs["NX_class"] = "NXinsertion_device" source.create_dataset(name="type", data="undulator") gap = source.create_dataset(name="gap", data=get_entry(data, "idgap")) gap.attrs["units"] = "mm" k = source.create_dataset(name="k", data=2.46) k.attrs["units"] = "NX_DIMENSIONLESS" length = source.create_dataset(name="length", data=1820) length.attrs["units"] = "mm" slit_0 = instrument.create_group("slit_0") slit_0.attrs["NX_class"] = "NXslit" source.create_dataset(name="material", data="OFHC Cu") source.create_dataset(name="description", data="Horizontal secondary source slit") x_gap = source.create_dataset(name="x_gap", data=get_entry(data, "sl0wh")) x_gap.attrs["units"] = "mm" x_translation = source.create_dataset(name="x_translation", data=get_entry(data, "sl0ch")) x_translation.attrs["units"] = "mm" distance = source.create_dataset( name="distance", data=-21700 - np.asarray(get_entry(data, "samz", 0)) ) distance.attrs["units"] = "mm" slit_1 = instrument.create_group("slit_1") slit_1.attrs["NX_class"] = "NXslit" source.create_dataset(name="material", data="OFHC Cu") source.create_dataset(name="description", data="Horizontal secondary source slit") x_gap = source.create_dataset(name="x_gap", data=get_entry(data, "sl1wh")) x_gap.attrs["units"] = "mm" y_gap = source.create_dataset(name="y_gap", data=get_entry(data, "sl1wv")) y_gap.attrs["units"] = "mm" x_translation = source.create_dataset(name="x_translation", data=get_entry(data, "sl1ch")) x_translation.attrs["units"] = "mm" height = source.create_dataset(name="x_translation", data=get_entry(data, "sl1ch")) height.attrs["units"] = "mm" distance = source.create_dataset( name="distance", data=-7800 - np.asarray(get_entry(data, "samz", 0)) ) distance.attrs["units"] = "mm" mono = instrument.create_group("monochromator") mono.attrs["NX_class"] = "NXmonochromator" mokev = data.get("mokev", {}) if mokev: if isinstance(mokev, list): mokev = mokev[0] wavelength = mono.create_dataset( name="wavelength", data=12.3984193 / (mokev.get("mokev").get("value") + 1e-9), ) wavelength.attrs["units"] = "Angstrom" energy = mono.create_dataset(name="energy", data=mokev.get("mokev").get("value")) energy.attrs["units"] = "keV" mono.create_dataset(name="type", data="Double crystal fixed exit monochromator.") distance = mono.create_dataset( name="distance", data=-5220 - np.asarray(get_entry(data, "samz", 0)) ) distance.attrs["units"] = "mm" crystal_1 = mono.create_group("crystal_1") crystal_1.attrs["NX_class"] = "NXcrystal" crystal_1.create_dataset(name="usage", data="Bragg") crystal_1.create_dataset(name="order_no", data="1") crystal_1.create_dataset(name="reflection", data="[1 1 1]") bragg_angle = crystal_1.create_dataset(name="bragg_angle", data=get_entry(data, "moth1")) bragg_angle.attrs["units"] = "degrees" crystal_2 = mono.create_group("crystal_2") crystal_2.attrs["NX_class"] = "NXcrystal" crystal_2.create_dataset(name="usage", data="Bragg") crystal_2.create_dataset(name="order_no", data="2") crystal_2.create_dataset(name="reflection", data="[1 1 1]") bragg_angle = crystal_2.create_dataset(name="bragg_angle", data=get_entry(data, "moth1")) bragg_angle.attrs["units"] = "degrees" bend_x = crystal_2.create_dataset(name="bend_x", data=get_entry(data, "mobd")) bend_x.attrs["units"] = "degrees" xbpm4 = instrument.create_group("XBPM4") xbpm4.attrs["NX_class"] = "NXdetector" xbpm4_sum = xbpm4.create_group("XBPM4_sum") xbpm4_sum_data = xbpm4_sum.create_dataset(name="data", data=get_entry(data, "bpm4s")) xbpm4_sum_data.attrs["units"] = "NX_DIMENSIONLESS" xbpm4_sum.create_dataset(name="description", data="Sum of counts for the four quadrants.") xbpm4_x = xbpm4.create_group("XBPM4_x") xbpm4_x_data = xbpm4_x.create_dataset(name="data", data=get_entry(data, "bpm4x")) xbpm4_x_data.attrs["units"] = "NX_DIMENSIONLESS" xbpm4_x.create_dataset( name="description", data="Normalized difference of counts between left and right quadrants.", ) xbpm4_y = xbpm4.create_group("XBPM4_y") xbpm4_y_data = xbpm4_y.create_dataset(name="data", data=get_entry(data, "bpm4y")) xbpm4_y_data.attrs["units"] = "NX_DIMENSIONLESS" xbpm4_y.create_dataset( name="description", data="Normalized difference of counts between high and low quadrants.", ) xbpm4_skew = xbpm4.create_group("XBPM4_skew") xbpm4_skew_data = xbpm4_skew.create_dataset(name="data", data=get_entry(data, "bpm4z")) xbpm4_skew_data.attrs["units"] = "NX_DIMENSIONLESS" xbpm4_skew.create_dataset( name="description", data="Normalized difference of counts between diagonal quadrants.", ) mirror = instrument.create_group("mirror") mirror.attrs["NX_class"] = "NXmirror" mirror.create_dataset(name="type", data="single") mirror.create_dataset( name="description", data="Grazing incidence mirror to reject high-harmonic wavelengths from the monochromator. There are three coating options available that are used depending on the X-ray energy, no coating (SiO2), rhodium (Rh) or platinum (Pt).", ) incident_angle = mirror.create_dataset(name="incident_angle", data=get_entry(data, "mith")) incident_angle.attrs["units"] = "degrees" substrate_material = mirror.create_dataset(name="substrate_material", data="SiO2") substrate_material.attrs["units"] = "NX_CHAR" coating_material = mirror.create_dataset(name="coating_material", data="SiO2") coating_material.attrs["units"] = "NX_CHAR" bend_y = mirror.create_dataset(name="bend_y", data="mibd") bend_y.attrs["units"] = "NX_DIMENSIONLESS" distance = mirror.create_dataset( name="distance", data=-4370 - np.asarray(get_entry(data, "samz", 0)) ) distance.attrs["units"] = "mm" xbpm5 = instrument.create_group("XBPM5") xbpm5.attrs["NX_class"] = "NXdetector" xbpm5_sum = xbpm5.create_group("XBPM5_sum") xbpm5_sum_data = xbpm5_sum.create_dataset(name="data", data=get_entry(data, "bpm5s")) xbpm5_sum_data.attrs["units"] = "NX_DIMENSIONLESS" xbpm5_sum.create_dataset(name="description", data="Sum of counts for the four quadrants.") xbpm5_x = xbpm5.create_group("XBPM5_x") xbpm5_x_data = xbpm5_x.create_dataset(name="data", data=get_entry(data, "bpm5x")) xbpm5_x_data.attrs["units"] = "NX_DIMENSIONLESS" xbpm5_x.create_dataset( name="description", data="Normalized difference of counts between left and right quadrants.", ) xbpm5_y = xbpm5.create_group("XBPM5_y") xbpm5_y_data = xbpm5_y.create_dataset(name="data", data=get_entry(data, "bpm5y")) xbpm5_y_data.attrs["units"] = "NX_DIMENSIONLESS" xbpm5_y.create_dataset( name="description", data="Normalized difference of counts between high and low quadrants.", ) xbpm5_skew = xbpm5.create_group("XBPM5_skew") xbpm5_skew_data = xbpm5_skew.create_dataset(name="data", data=get_entry(data, "bpm5z")) xbpm5_skew_data.attrs["units"] = "NX_DIMENSIONLESS" xbpm5_skew.create_dataset( name="description", data="Normalized difference of counts between diagonal quadrants.", ) slit_2 = instrument.create_group("slit_2") slit_2.attrs["NX_class"] = "NXslit" source.create_dataset(name="material", data="Ag") source.create_dataset(name="description", data="Slit 2, optics hutch") x_gap = source.create_dataset(name="x_gap", data=get_entry(data, "sl2wh")) x_gap.attrs["units"] = "mm" y_gap = source.create_dataset(name="y_gap", data=get_entry(data, "sl2wv")) y_gap.attrs["units"] = "mm" x_translation = source.create_dataset(name="x_translation", data=get_entry(data, "sl2ch")) x_translation.attrs["units"] = "mm" height = source.create_dataset(name="x_translation", data=get_entry(data, "sl2cv")) height.attrs["units"] = "mm" distance = source.create_dataset( name="distance", data=-3140 - np.asarray(get_entry(data, "samz", 0)) ) distance.attrs["units"] = "mm" slit_3 = instrument.create_group("slit_3") slit_3.attrs["NX_class"] = "NXslit" source.create_dataset(name="material", data="Si") source.create_dataset(name="description", data="Slit 3, experimental hutch, exposure box") x_gap = source.create_dataset(name="x_gap", data=get_entry(data, "sl3wh")) x_gap.attrs["units"] = "mm" y_gap = source.create_dataset(name="y_gap", data=get_entry(data, "sl3wv")) y_gap.attrs["units"] = "mm" x_translation = source.create_dataset(name="x_translation", data=get_entry(data, "sl3ch")) x_translation.attrs["units"] = "mm" height = source.create_dataset(name="x_translation", data=get_entry(data, "sl3cv")) height.attrs["units"] = "mm" # distance = source.create_dataset(name="distance", data=-3140 - get_entry(data, "samz", 0)) # distance.attrs["units"] = "mm" filter_set = instrument.create_group("filter_set") filter_set.attrs["NX_class"] = "NXattenuator" filter_set.create_dataset(name="material", data="Si") filter_set.create_dataset( name="description", data="The filter set consists of 4 linear stages, each with five filter positions. Additionally, each one allows for an out position to allow 'no filtering'.", ) attenuator_transmission = filter_set.create_dataset( name="attenuator_transmission", data=10 ** get_entry(data, "ftrans", 0) ) attenuator_transmission.attrs["units"] = "NX_DIMENSIONLESS" slit_4 = instrument.create_group("slit_4") slit_4.attrs["NX_class"] = "NXslit" source.create_dataset(name="material", data="Si") source.create_dataset(name="description", data="Slit 4, experimental hutch, exposure box") x_gap = source.create_dataset(name="x_gap", data=get_entry(data, "sl4wh")) x_gap.attrs["units"] = "mm" y_gap = source.create_dataset(name="y_gap", data=get_entry(data, "sl4wv")) y_gap.attrs["units"] = "mm" x_translation = source.create_dataset(name="x_translation", data=get_entry(data, "sl4ch")) x_translation.attrs["units"] = "mm" height = source.create_dataset(name="x_translation", data=get_entry(data, "sl4cv")) height.attrs["units"] = "mm" # distance = source.create_dataset(name="distance", data=-3140 - get_entry(data, "samz", 0)) # distance.attrs["units"] = "mm" slit_5 = instrument.create_group("slit_5") slit_5.attrs["NX_class"] = "NXslit" source.create_dataset(name="material", data="Si") source.create_dataset(name="description", data="Slit 5, experimental hutch, exposure box") x_gap = source.create_dataset(name="x_gap", data=get_entry(data, "sl5wh")) x_gap.attrs["units"] = "mm" y_gap = source.create_dataset(name="y_gap", data=get_entry(data, "sl5wv")) y_gap.attrs["units"] = "mm" x_translation = source.create_dataset(name="x_translation", data=get_entry(data, "sl5ch")) x_translation.attrs["units"] = "mm" height = source.create_dataset(name="x_translation", data=get_entry(data, "sl5cv")) height.attrs["units"] = "mm" # distance = source.create_dataset(name="distance", data=-3140 - get_entry(data, "samz", 0)) # distance.attrs["units"] = "mm" beam_stop_1 = instrument.create_group("beam_stop_1") beam_stop_1.attrs["NX_class"] = "NX_beamstop" beam_stop_1.create_dataset(name="description", data="circular") bms1_size = beam_stop_1.create_dataset(name="size", data=3) bms1_size.attrs["units"] = "mm" bms1_x = beam_stop_1.create_dataset(name="size", data=get_entry(data, "bs1x")) bms1_x.attrs["units"] = "mm" bms1_y = beam_stop_1.create_dataset(name="size", data=get_entry(data, "bs1y")) bms1_y.attrs["units"] = "mm" beam_stop_2 = instrument.create_group("beam_stop_2") beam_stop_2.attrs["NX_class"] = "NX_beamstop" beam_stop_2.create_dataset(name="description", data="rectangular") bms2_size_x = beam_stop_2.create_dataset(name="size_x", data=5) bms2_size_x.attrs["units"] = "mm" bms2_size_y = beam_stop_2.create_dataset(name="size_y", data=2.25) bms2_size_y.attrs["units"] = "mm" bms2_x = beam_stop_2.create_dataset(name="size", data=get_entry(data, "bs2x")) bms2_x.attrs["units"] = "mm" bms2_y = beam_stop_2.create_dataset(name="size", data=get_entry(data, "bs2y")) bms2_y.attrs["units"] = "mm" bms2_data = beam_stop_2.create_dataset(name="data", data=get_entry(data, "diode")) bms2_data.attrs["units"] = "NX_DIMENSIONLESS" if "eiger1p5m" in device_manager.devices and device_manager.devices.eiger1p5m.enabled: eiger_4 = instrument.create_group("eiger_4") eiger_4.attrs["NX_class"] = "NXdetector" x_pixel_size = eiger_4.create_dataset(name="x_pixel_size", data=75) x_pixel_size.attrs["units"] = "um" y_pixel_size = eiger_4.create_dataset(name="y_pixel_size", data=75) y_pixel_size.attrs["units"] = "um" polar_angle = eiger_4.create_dataset(name="polar_angle", data=0) polar_angle.attrs["units"] = "degrees" azimuthal_angle = eiger_4.create_dataset(name="azimuthal_angle", data=0) azimuthal_angle.attrs["units"] = "degrees" rotation_angle = eiger_4.create_dataset(name="rotation_angle", data=0) rotation_angle.attrs["units"] = "degrees" description = eiger_4.create_dataset( name="description", data="Single-photon counting detector, 320 micron-thick Si chip", ) orientation = eiger_4.create_group("orientation") orientation.attrs["description"] = ( "Orientation defines the number of counterclockwise rotations by 90 deg followed by a transposition to reach the 'cameraman orientation', that is looking towards the beam." ) orientation.create_dataset(name="transpose", data=1) orientation.create_dataset(name="rot90", data=3) if ( "eiger9m" in device_manager.devices and device_manager.devices.eiger9m.enabled and "eiger9m" in file_references ): eiger9m = instrument.create_group("eiger9m") eiger9m.attrs["NX_class"] = "NXdetector" x_pixel_size = eiger9m.create_dataset(name="x_pixel_size", data=75) x_pixel_size.attrs["units"] = "um" y_pixel_size = eiger9m.create_dataset(name="y_pixel_size", data=75) y_pixel_size.attrs["units"] = "um" polar_angle = eiger9m.create_dataset(name="polar_angle", data=0) polar_angle.attrs["units"] = "degrees" azimuthal_angle = eiger9m.create_dataset(name="azimuthal_angle", data=0) azimuthal_angle.attrs["units"] = "degrees" rotation_angle = eiger9m.create_dataset(name="rotation_angle", data=0) rotation_angle.attrs["units"] = "degrees" description = eiger9m.create_dataset( name="description", data="Eiger9M detector, in-house developed, Paul Scherrer Institute", ) orientation = eiger9m.create_group("orientation") orientation.attrs["description"] = ( "Orientation defines the number of counterclockwise rotations by 90 deg followed by a transposition to reach the 'cameraman orientation', that is looking towards the beam." ) orientation.create_dataset(name="transpose", data=1) orientation.create_dataset(name="rot90", data=3) data = eiger9m.create_ext_link("data", file_references["eiger9m"]["path"], "EG9M/data") status = eiger9m.create_ext_link( "status", file_references["eiger9m"]["path"], "EG9M/status" ) if ( "pilatus_2" in device_manager.devices and device_manager.devices.pilatus_2.enabled and "pilatus_2" in file_references ): pilatus_2 = instrument.create_group("pilatus_2") pilatus_2.attrs["NX_class"] = "NXdetector" x_pixel_size = pilatus_2.create_dataset(name="x_pixel_size", data=172) x_pixel_size.attrs["units"] = "um" y_pixel_size = pilatus_2.create_dataset(name="y_pixel_size", data=172) y_pixel_size.attrs["units"] = "um" polar_angle = pilatus_2.create_dataset(name="polar_angle", data=0) polar_angle.attrs["units"] = "degrees" azimuthal_angle = pilatus_2.create_dataset(name="azimuthal_angle", data=0) azimuthal_angle.attrs["units"] = "degrees" rotation_angle = pilatus_2.create_dataset(name="rotation_angle", data=0) rotation_angle.attrs["units"] = "degrees" description = pilatus_2.create_dataset( name="description", data="Pilatus 300K detector, Dectris, Switzerland" ) orientation = pilatus_2.create_group("orientation") orientation.attrs["description"] = ( "Orientation defines the number of counterclockwise rotations by 90 deg followed by a transposition to reach the 'cameraman orientation', that is looking towards the beam." ) orientation.create_dataset(name="transpose", data=1) orientation.create_dataset(name="rot90", data=2) data = pilatus_2.create_ext_link( "data", file_references["pilatus_2"]["path"], "entry/instrument/pilatus_2/data", ) if ( "falcon" in device_manager.devices and device_manager.devices.falcon.enabled and "falcon" in file_references ): falcon = instrument.create_ext_link( "falcon", file_references["falcon"]["path"], "entry/instrument/FalconX1", ) return storage