diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 68321144e096b13ec738d928d6a83a47d9e2383c..10451786594e201dcbc462780a314b45b47bcd39 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,9 +65,9 @@ doc: - python -m pip install setuptools --upgrade - source ./ci/install_scripts.sh - install_silx - - install_tomoscan - python -m pip install pytest-cov - python -m pip install -e . + - install_tomoscan script: - python -m pytest --cov-config=.coveragerc --cov=nxtomomill nxtomomill/ @@ -89,6 +89,7 @@ test:test-nxtomomill-tutorials: - export PYTHONPATH="${PYTHONPATH}:/usr/lib/python3/dist-packages/" - export LD_LIBRARY_PATH=/lib/i386-linux-gnu/:${LD_LIBRARY_PATH} - export LD_LIBRARY_PATH=/lib/x86_64-linux-gnu/:${LD_LIBRARY_PATH} + - source ./ci/install_scripts.sh - python --version - python -m pip install pip --upgrade - python -m pip install setuptools --upgrade @@ -97,6 +98,7 @@ test:test-nxtomomill-tutorials: - python -m pip install silx --upgrade --pre - python -m pip install tomoscan --upgrade --pre - python -m pip install .[doc] + - install_tomoscan script: - jupyter nbconvert --to notebook --execute doc/tutorials/nx_tomo_tutorial.ipynb diff --git a/ci/install_scripts.sh b/ci/install_scripts.sh index ed3301eee10c075427068332c88e0bc2da140e31..81e70950fbb85092da3c4edd5f9ea51a60f23a46 100644 --- a/ci/install_scripts.sh +++ b/ci/install_scripts.sh @@ -1,7 +1,7 @@ #!/bin/bash function install_tomoscan(){ - python -m pip install git+https://gitlab.esrf.fr/tomotools/tomoscan.git + python -m pip install git+https://gitlab.esrf.fr/tomotools/tomoscan.git@add_current } function install_silx(){ diff --git a/nxtomomill/converter/edf/edfconverter.py b/nxtomomill/converter/edf/edfconverter.py index 2fcc56473825dc432d4e500871b5ac2c582dd183..166f7598e45be2bc28529c96a8673091f97cf23c 100644 --- a/nxtomomill/converter/edf/edfconverter.py +++ b/nxtomomill/converter/edf/edfconverter.py @@ -40,11 +40,12 @@ from nxtomomill.io.config.edfconfig import TomoEDFConfig from nxtomomill.nexus.nxsource import SourceType from nxtomomill import utils from nxtomomill.utils import ImageKey -from nxtomomill.converter.version import version as converter_version +from nxtomomill.converter.version import LATEST_VERSION, version as converter_version from nxtomomill.nexus.utils import create_nx_data_group from nxtomomill.nexus.utils import link_nxbeam_to_root from nxtomomill.nexus.nxdetector import FieldOfView from nxtomomill.settings import Tomo +from tomoscan.nexus.paths.nxtomo import get_paths as get_nexus_paths from silx.utils.deprecation import deprecated from tomoscan.esrf.utils import get_parameters_frm_par_or_info from tomoscan.unitsystem.energysystem import EnergySI @@ -69,6 +70,7 @@ EDF_DARK_NAMES = Tomo.EDF.DARK_NAMES EDF_X_TRANS = Tomo.EDF.X_TRANS EDF_Y_TRANS = Tomo.EDF.Y_TRANS EDF_Z_TRANS = Tomo.EDF.Z_TRANS +EDF_MACHINE_ELECTRIC_CURRENT = Tomo.EDF.MACHINE_ELECTRIC_CURRENT _logger = logging.getLogger(__name__) @@ -85,6 +87,7 @@ EDFFileKeys = namedtuple( "to_ignore", "dark_names", "ref_names", + "machine_elec_current_keys", ], ) @@ -98,6 +101,7 @@ DEFAULT_EDF_KEYS = EDFFileKeys( EDF_TO_IGNORE, EDF_DARK_NAMES, EDF_REFS_NAMES, + EDF_MACHINE_ELECTRIC_CURRENT, ) @@ -151,6 +155,7 @@ def edf_to_nx( config.x_trans_keys = file_keys.x_trans_keys config.y_trans_keys = file_keys.y_trans_keys config.z_trans_keys = file_keys.z_trans_keys + config.machine_electric_current_keys = file_keys.machine_elec_current_keys config.ignore_file_patterns = file_keys.to_ignore config.dark_names = file_keys.dark_names config.flat_names = file_keys.ref_names @@ -197,6 +202,16 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: ) _logger.info(f"Output file will be {fileout_h5}") + default_current = scan.retrieve_information( + scan=scan.path, + dataset_basename=scan.dataset_basename, + ref_file=None, + key="SrCurrent", + key_aliases=["SRCUR", "machineCurrentStart"], + type_=float, + scan_info=scan.scan_info, + ) + output_data_path = "entry" DARK_ACCUM_FACT = True @@ -249,6 +264,7 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: xtrans_index = -1 ytrans_index = -1 ztrans_index = -1 + srcur_index = -1 frame_type = None try: @@ -258,13 +274,16 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: hd = fid.getHeader() motor_mne_key = _get_valid_key(hd, configuration.motor_mne_keys) motors = hd.get(motor_mne_key, "").split(" ") - + counters = hd.get("counter_mne", "").split(" ") rotangle_index = _get_valid_key_index( motors, configuration.rotation_angle_keys ) xtrans_index = _get_valid_key_index(motors, configuration.x_trans_keys) ytrans_index = _get_valid_key_index(motors, configuration.y_trans_keys) ztrans_index = _get_valid_key_index(motors, configuration.z_trans_keys) + srcur_index = _get_valid_key_index( + counters, configuration.machine_electric_current_keys + ) if hasattr(fid, "bytecode"): frame_type = fid.bytecode @@ -274,7 +293,14 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: fid.close() fid = None - return frame_type, rotangle_index, xtrans_index, ytrans_index, ztrans_index + return ( + frame_type, + rotangle_index, + xtrans_index, + ytrans_index, + ztrans_index, + srcur_index, + ) ( frame_type, @@ -282,6 +308,7 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: x_trans_index, y_trans_index, z_trans_index, + srcur_index, ) = getExtraInfo(scan=scan) if rot_angle_index == -1 and configuration.force_angle_calculation is False: @@ -390,6 +417,14 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: ) h5d["/entry/sample/rotation_angle"].attrs["unit"] = "degree" + nexus_paths = get_nexus_paths(LATEST_VERSION) + electric_current_dataset = h5d.create_dataset( + "/".join(["entry", nexus_paths.ELECTRIC_CURRENT_PATH]), + shape=(n_frames,), + dtype=numpy.float32, + ) + h5d["/entry/sample/rotation_angle"].attrs["unit"] = "degree" + # provision for centering motors x_dataset = h5d.create_dataset( "/entry/sample/x_translation", shape=(n_frames,), dtype=numpy.float32 @@ -514,6 +549,15 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: * metricsystem.millimeter.value ) + if srcur_index == -1: + electric_current_dataset[nf] = default_current + else: + try: + str_counter_val = header.get("counter_pos", "").split(" ") + electric_current_dataset[nf] = float(str_counter_val[srcur_index]) + except IndexError: + electric_current_dataset[nf] = default_current + nf += 1 if progress is not None: progress.increase_advancement(i=1) @@ -582,8 +626,19 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: zDataset[nfr] = 0.0 else: zDataset[nfr] = float(str_mot_val[ztix]) + if srcur_index == -1: + electric_current_dataset[nfr] = default_current + else: + str_counter_val = header.get("counter_pos", "").split(" ") + try: + electric_current_dataset[nfr] = float( + str_counter_val[srcur_index] + ) + except IndexError: + electric_current_dataset[nfr] = default_current nfr += 1 + return nfr # projections @@ -672,7 +727,14 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: z_dataset[nf] = 0.0 else: z_dataset[nf] = float(str_mot_val[z_trans_index]) - + if srcur_index == -1: + electric_current_dataset[nf] = default_current + else: + try: + str_counter_val = header.get("counter_pos", "").split(" ") + electric_current_dataset[nf] = float(str_counter_val[srcur_index]) + except IndexError: + electric_current_dataset[nf] = default_current nf += 1 nproj += 1 diff --git a/nxtomomill/converter/hdf5/acquisition/baseacquisition.py b/nxtomomill/converter/hdf5/acquisition/baseacquisition.py index e4179257825c056c6d669295c15efa2a16da3937..f4e37b2427444d52f1e84a9579255e28301ffa75 100644 --- a/nxtomomill/converter/hdf5/acquisition/baseacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/baseacquisition.py @@ -410,7 +410,29 @@ class BaseAcquisition: def _get_positioners_node(entry_node): if not isinstance(entry_node, h5py.Group): raise TypeError("entry_node is expected to be a h5py.Group") - return BaseAcquisition._get_instrument_node(entry_node)["positioners"] + parent_node = BaseAcquisition._get_instrument_node(entry_node) + if "positioners" in parent_node: + return parent_node["positioners"] + else: + return None + + @staticmethod + def _get_measurement_node(entry_node): + if not isinstance(entry_node, h5py.Group): + raise TypeError("entry_node is expected to be a h5py.Group") + if "measurement" in entry_node: + return entry_node["measurement"] + else: + return None + + @staticmethod + def _get_machine_node(entry_node): + if not isinstance(entry_node, h5py.Group): + raise TypeError("entry_node is expected to be a h5py.Group") + if "instrument/machine" in entry_node: + return entry_node["instrument/machine"] + else: + return None def _read_rotation_motor_name(self) -> typing.Union[str, None]: """read rotation motor from root_url/technique/scan/motor @@ -443,6 +465,49 @@ class BaseAcquisition: ) return None + def _get_electric_current(self, root_node) -> list: + """retrieve electric current provide a time stamp for each of them""" + if self._root_url is None: + _logger.warning("no root url. Unable to read rotation motor") + return None, None + else: + grps = [ + root_node, + ] + + measurement_node = self._get_measurement_node(root_node) + if measurement_node is not None: + grps.append(measurement_node) + machine_node = self._get_machine_node(root_node) + if machine_node is not None: + grps.append(machine_node) + + for grp in grps: + try: + elec_current, unit = self._get_node_values_for_frame_array( + node=grp, + keys=self.configuration.machine_electric_current_keys, + info_retrieve="machine electric current", + expected_unit="mA", + n_frame=None, + ) + except (ValueError, KeyError): + pass + else: + # handle case where elec_current is a scalar. Cast it to list before return + if isinstance(elec_current, numpy.number): + elec_current = [ + elec_current, + ] + + return elec_current, unit + else: + _logger.warning( + f"Unable to retrieve machine electric current for {root_node.name}" + ) + + return None, None + def _get_rotation_angle(self, root_node, n_frame) -> tuple: """return the list of rotation angle for each frame""" if not isinstance(root_node, h5py.Group): diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index 67646d87f4689b9f9a8f9ca3770ef8762adf37da..7309ed29e623b003375af8c646dff5aebfd0f548 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -34,11 +34,14 @@ __license__ = "MIT" __date__ = "14/02/2022" +from datetime import datetime + +from nxtomomill.utils.utils import str_datetime_to_numpy_datetime64 from .baseacquisition import BaseAcquisition from nxtomomill.nexus.nxsource import SourceType from nxtomomill.io.acquisitionstep import AcquisitionStep from .baseacquisition import EntryReader -from .utils import get_entry_type +from .utils import deduce_machine_electric_current, get_entry_type from .utils import guess_nx_detector from .utils import get_nx_detectors from nxtomomill.utils import ImageKey @@ -47,6 +50,7 @@ from silx.io.utils import h5py_read_dataset import h5py from silx.io.url import DataUrl from typing import Optional, Union +from tomoscan.unitsystem import ElectricCurrentSystem try: import hdf5plugin # noqa F401 @@ -99,15 +103,14 @@ class StandardAcquisition(BaseAcquisition): """y_translation""" self._z_translation = None - # self._n_frames = None - # self._dim_1 = None - # self._dim_2 = None - # self._data_type = None self._virtual_sources = None self._acq_expo_time = None self._copied_dataset = {} "register dataset copied. Key if the original location as" "DataUrl.path. Value is the DataUrl it has been moved to" - # self._current_scan_n_frame = None + self._known_machine_electric_current = None + # store all registred amchine electric current + self._frames_timestamp = None + # try to deduce time stamp of each frame def get_expected_nx_tomo(self): return 1 @@ -136,6 +139,10 @@ class StandardAcquisition(BaseAcquisition): def n_frames(self): return self._n_frames + @property + def n_frames_actual_bliss_scan(self): + return self._n_frames_actual_bliss_scan + @property def dim_1(self): return self._dim_1 @@ -152,6 +159,13 @@ class StandardAcquisition(BaseAcquisition): def expo_time(self): return self._acq_expo_time + @property + def known_machine_electric_current(self) -> Optional[dict]: + """ + Return the dict of all know machine electric current. Key is the time stamp, value is the electric current + """ + return self._known_machine_electric_current + @property def is_xrd_ct(self): return False @@ -266,6 +280,7 @@ class StandardAcquisition(BaseAcquisition): n_frame = shape[0] self._n_frames += n_frame + self._n_frames_actual_bliss_scan = n_frame if self.dim_1 is None: self._dim_2 = shape[1] self._dim_1 = shape[2] @@ -306,7 +321,10 @@ class StandardAcquisition(BaseAcquisition): input_file_path, entry_path, entry_url, - ): + ) -> bool: + """ + return True if the entry contains frames + """ if "data_cast" in detector_node: _logger.warning( "!!! looks like this data has been cast. Take cast data for %s!!!" @@ -414,7 +432,7 @@ class StandardAcquisition(BaseAcquisition): if "instrument" not in entry: _logger.error( - "no instrument group found in %s, unable to" + "no instrument group found in %s, unable to " "retrieve frames" % entry.name ) return @@ -426,7 +444,8 @@ class StandardAcquisition(BaseAcquisition): # update valid camera names self.configuration.valid_camera_names = det_grps - for key, det_grp in instrument_grp.items(): + has_frames = False + for key, _ in instrument_grp.items(): if ( "NX_class" in instrument_grp[key].attrs and instrument_grp[key].attrs["NX_class"] == "NXdetector" @@ -452,7 +471,6 @@ class StandardAcquisition(BaseAcquisition): continue else: detector_node = instrument_grp[key] - _logger.info(f"start treatment of detector {detector_node}") self._treate_valid_camera( detector_node, entry=entry, @@ -461,11 +479,127 @@ class StandardAcquisition(BaseAcquisition): entry_path=entry_path, entry_url=entry_url, ) + has_frames = True + # try to get some other metadata + + # handle frame time stamp + start_time = self._get_start_time(entry) + if start_time is not None: + start_time = datetime.fromisoformat(start_time) + end_time = self._get_end_time(entry) + if end_time is not None: + end_time = datetime.fromisoformat(end_time) + + if has_frames: + self._register_frame_timestamp(entry, start_time, end_time) + + # handle electric current. Can retrieve some current even on bliss scan entry doesn;t containing directly frames + self._register_machine_electric_current(entry, start_time, end_time) + + def _register_machine_electric_current( + self, entry: h5py.Group, start_time, end_time + ): + """Update machine electric current for provided entry (bliss scan""" + ( + electric_currents, + electric_current_unit, + ) = self._get_electric_current(root_node=entry) + electric_current_unit_ref = ElectricCurrentSystem.AMPERE + # electric current will be saved as Ampere + if electric_currents is not None and len(electric_currents) > 0: + if electric_current_unit is None: + electric_current_unit = ElectricCurrentSystem.MILLIAMPERE + _logger.warning( + "No unit found for electric current. Consider it as mA." + ) + + unit_factor = ( + ElectricCurrentSystem.from_str(electric_current_unit).value + / electric_current_unit_ref.value + ) + + new_know_electric_currents = {} + if start_time is None or end_time is None: + if start_time != end_time: + _logger.warning( + f"Unable to find {'start_time' if start_time is None else 'end_time'}. Will pick the first available electric_current for the frame" + ) + t_time = start_time or end_time + # if at least one can find out + new_know_electric_currents[ + str_datetime_to_numpy_datetime64(t_time) + ] = (electric_currents[0] * unit_factor) + else: + _logger.error( + "Unable to find start_time and end_time. Will not register any machine electric current" + ) + elif len(electric_currents) == 1: + # if we have only one value, consider the machine electric current is constant during this time + # might be improved later if we can know if current is determine at the + # beginning or the end. But should have no impact + # as the time slot is short + new_know_electric_currents[ + str_datetime_to_numpy_datetime64(start_time) + ] = (electric_currents[0] * unit_factor) + else: + # linspace from datetime within ms precision. + # see https://localcoder.org/creating-numpy-linspace-out-of-datetime#credit_4 + # and https://stackoverflow.com/questions/37964100/creating-numpy-linspace-out-of-datetime + timestamps = numpy.linspace( + start=str_datetime_to_numpy_datetime64(start_time).astype( + numpy.float128 + ), + stop=str_datetime_to_numpy_datetime64(end_time).astype( + numpy.float128 + ), + num=len(electric_currents), + endpoint=True, + dtype=" tuple: self._preprocess_registered_entries() - nx_tomo = NXtomo("/") + nx_tomo = NXtomo() # 1. root level information # start and end time @@ -834,7 +982,7 @@ class StandardAcquisition(BaseAcquisition): # define tomo_n nx_tomo.instrument.detector.tomo_n = self._get_tomo_n() - # 3. define nx source + # 4. define nx source source_name = self._get_source_name() nx_tomo.instrument.source.name = source_name source_type = self._get_source_type() @@ -849,7 +997,7 @@ class StandardAcquisition(BaseAcquisition): nx_tomo.instrument.source.type = source_type - # 4. define sample + # 5. define sample nx_tomo.sample.name = self._get_sample_name() nx_tomo.sample.rotation_angle = self.rotation_angle nx_tomo.sample.x_translation.value = self.x_translation @@ -859,7 +1007,18 @@ class StandardAcquisition(BaseAcquisition): nx_tomo.sample.z_translation.value = self.z_translation nx_tomo.sample.z_translation.unit = "m" - # 5. define diode + # 6. define control + if self.known_machine_electric_current not in (None, dict()): + nx_tomo.control.data = deduce_machine_electric_current( + timestamps=self._frames_timestamp, + knowned_machine_electric_current=self._known_machine_electric_current, + ) + nx_tomo.control.data.unit = ElectricCurrentSystem.AMPERE + types = set() + for d in nx_tomo.control.data.value: + types.add(type(d)) + + # 7. define diode if self.has_diode: nx_tomo.instrument.diode.data = self._diode nx_tomo.instrument.diode.data.unit = self._diode_unit diff --git a/nxtomomill/converter/hdf5/acquisition/test/test_acquisition_utils.py b/nxtomomill/converter/hdf5/acquisition/test/test_acquisition_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f17bb908cf76fb328c1de8d40e4efc978c164f36 --- /dev/null +++ b/nxtomomill/converter/hdf5/acquisition/test/test_acquisition_utils.py @@ -0,0 +1,117 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ + +__authors__ = ["H. Payno"] +__license__ = "MIT" +__date__ = "28/04/2022" + + +import numpy +import pytest +from nxtomomill.converter.hdf5.acquisition.utils import deduce_machine_electric_current +from nxtomomill.utils.utils import str_datetime_to_numpy_datetime64 + + +def test_deduce_machine_electric_current(): + """ + Test `deduce_electric_current` function. Base function to compute current for each frame according to it's timestamp + """ + + electric_current_datetimes = { + "2022-01-15T21:07:58.360095+02:00": 1.1, + "2022-04-15T21:07:58.360095+02:00": 121.1, + "2022-04-15T21:09:58.360095+02:00": 123.3, + "2022-04-15T21:11:58.360095+02:00": 523.3, + "2022-12-15T21:07:58.360095+02:00": 1000.3, + } + with pytest.raises(ValueError): + deduce_machine_electric_current(tuple(), {}) + with pytest.raises(TypeError): + deduce_machine_electric_current(12, 2) + with pytest.raises(TypeError): + deduce_machine_electric_current( + [ + 12, + ], + 2, + ) + with pytest.raises(TypeError): + deduce_machine_electric_current( + [ + 2, + ], + electric_current_datetimes, + ) + + converted_electric_currents = {} + for elec_cur_datetime_str, elect_cur in electric_current_datetimes.items(): + datetime_as_datetime = str_datetime_to_numpy_datetime64(elec_cur_datetime_str) + converted_electric_currents[datetime_as_datetime] = elect_cur + + # check exacts values, left and right bounds + assert deduce_machine_electric_current( + (str_datetime_to_numpy_datetime64("2022-01-15T21:07:58.360095+02:00"),), + converted_electric_currents, + ) == (1.1,) + assert deduce_machine_electric_current( + (str_datetime_to_numpy_datetime64("2022-01-14T21:07:58.360095+02:00"),), + converted_electric_currents, + ) == (1.1,) + assert deduce_machine_electric_current( + (str_datetime_to_numpy_datetime64("2022-12-15T21:07:58.360095+02:00"),), + converted_electric_currents, + ) == (1000.3,) + assert deduce_machine_electric_current( + (str_datetime_to_numpy_datetime64("2022-12-16T21:07:58.360095+02:00"),), + converted_electric_currents, + ) == (1000.3,) + # check interpolated values + numpy.testing.assert_almost_equal( + deduce_machine_electric_current( + (str_datetime_to_numpy_datetime64("2022-04-15T21:08:58.360095+02:00"),), + converted_electric_currents, + )[0], + (122.2,), + ) + numpy.testing.assert_almost_equal( + deduce_machine_electric_current( + (str_datetime_to_numpy_datetime64("2022-04-15T21:10:28.360095+02:00"),), + converted_electric_currents, + )[0], + (223.3,), + ) + + # test several call and insure keep order + numpy.testing.assert_almost_equal( + deduce_machine_electric_current( + ( + str_datetime_to_numpy_datetime64("2022-01-15T21:07:58.360095+02:00"), + str_datetime_to_numpy_datetime64("2022-04-15T21:10:28.360095+02:00"), + str_datetime_to_numpy_datetime64("2022-04-15T21:08:58.360095+02:00"), + ), + converted_electric_currents, + ), + (1.1, 223.3, 122.2), + ) diff --git a/nxtomomill/converter/hdf5/acquisition/utils.py b/nxtomomill/converter/hdf5/acquisition/utils.py index 0db64e5f25c5e15246d9a337196556e1cb2dcdb7..37d7087d531f04115f51cda07157648f749ac46c 100644 --- a/nxtomomill/converter/hdf5/acquisition/utils.py +++ b/nxtomomill/converter/hdf5/acquisition/utils.py @@ -28,11 +28,13 @@ Utils related to bliss-HDF5 """ +import numpy from nxtomomill.io.acquisitionstep import AcquisitionStep from silx.io.utils import h5py_read_dataset from nxtomomill.io.config import TomoHDF5Config from silx.io.url import DataUrl from tomoscan.io import HDF5File +from bisect import bisect_left import typing import h5py @@ -143,3 +145,65 @@ def guess_nx_detector(node: h5py.Group) -> tuple: nx_detectors = sorted(nx_detectors, key=lambda det: det.name) return tuple(nx_detectors) + + +def deduce_machine_electric_current( + timestamps: tuple, knowned_machine_electric_current: dict +) -> dict: + """ + :param dict knowned_machine_electric_current: keys are electric timestamp. Value is electric current + :param tuple timestamp: keys are frame index. timestamp. Value is electric current + """ + if not isinstance(knowned_machine_electric_current, dict): + raise TypeError("knowned_machine_electric_current is expected to be a dict") + for elmt in timestamps: + if not isinstance(elmt, numpy.datetime64): + raise TypeError( + f"elmts of timestamps are expected to be {numpy.datetime64} and not {type(elmt)}" + ) + if len(knowned_machine_electric_current) == 0: + raise ValueError( + "knowned_machine_electric_current should at least contains one element" + ) + for key, value in knowned_machine_electric_current.items(): + if not isinstance(key, numpy.datetime64): + raise TypeError( + f"knowned_machine_electric_current keys are expected to be instances of {numpy.datetime64} and not {type(key)}" + ) + if not isinstance(value, (float, numpy.number)): + raise TypeError( + "knowned_machine_electric_current values are expected to be instances of float" + ) + + know_timestamps = sorted(knowned_machine_electric_current.keys()) + + def get_closest_timestamps(timestamp) -> tuple: + if timestamp in know_timestamps: + return (timestamp, 1.0), (None, 0.0) + + pos = bisect_left(know_timestamps, timestamp) + left_timestamp = know_timestamps[pos - 1] + if pos == 0: + return (know_timestamps[0], 1.0), (None, 0.0) + elif pos > len(know_timestamps) - 1: + return (know_timestamps[-1], 1.0), (None, 0.0) + else: + right_timestamp = know_timestamps[pos] + delta = right_timestamp - left_timestamp + return ( + (left_timestamp, 1 - (timestamp - left_timestamp) / delta), + (right_timestamp, 1 - (right_timestamp - timestamp) / delta), + ) + + res = [] + for timestamp in timestamps: + if not isinstance(timestamp, numpy.datetime64): + raise TypeError( + f"elements of timestamps are expected to be instances of {numpy.datetime64} and not {type(timestamp)}" + ) + (ts1, w1), (ts2, w2) = get_closest_timestamps(timestamp) + ec1 = knowned_machine_electric_current.get(ts1) + assert ec1 is not None + ec2 = knowned_machine_electric_current.get(ts2, 0.0) + res.append(ec1 * w1 + ec2 * w2) + return tuple(res) diff --git a/nxtomomill/converter/hdf5/hdf5converter.py b/nxtomomill/converter/hdf5/hdf5converter.py index 5173f5055e9de7fe9aaf9ebd9f4f752d9b2dd4b3..de608f5db0b547eb7af4a6e5f9081592c77a2c5b 100644 --- a/nxtomomill/converter/hdf5/hdf5converter.py +++ b/nxtomomill/converter/hdf5/hdf5converter.py @@ -684,6 +684,8 @@ class _H5ToNxConverter(BaseConverter): def _check_conversion_is_possible(self): """Insure minimalistic information are provided""" if self.configuration.is_using_titles: + if self.configuration.input_file is None: + raise ValueError("input file should be provided") if not os.path.isfile(self.configuration.input_file): raise ValueError( "Given input file does not exists: {}" diff --git a/nxtomomill/converter/hdf5/test/test_hdf5converter.py b/nxtomomill/converter/hdf5/test/test_hdf5converter.py index 18afe23118bdb05e0cfc1f74881d9eef6c687da7..553dbc51913a6a4e508fd456c3b2b23a3db397ae 100644 --- a/nxtomomill/converter/hdf5/test/test_hdf5converter.py +++ b/nxtomomill/converter/hdf5/test/test_hdf5converter.py @@ -25,7 +25,7 @@ __authors__ = ["H. Payno"] __license__ = "MIT" -__date__ = "09/10/2020" +__date__ = "02/05/2022" import unittest @@ -40,12 +40,14 @@ from nxtomomill.converter.hdf5.acquisition.utils import guess_nx_detector from nxtomomill.utils.hdf5 import EntryReader from nxtomomill.utils.hdf5 import DatasetReader from nxtomomill.io.config import TomoHDF5Config +from nxtomomill.test.utils.bliss import _BlissSample try: from tomoscan.esrf.scan.hdf5scan import HDF5TomoScan except ImportError: from tomoscan.esrf.hdf5scan import HDF5TomoScan from tomoscan.validator import is_valid_for_reconstruction +from tomoscan.esrf.hdf5scan import ImageKey from nxtomomill.test.utils.bliss import MockBlissAcquisition from silx.io.url import DataUrl from silx.io.utils import get_data @@ -1005,3 +1007,135 @@ def test_simple_conversion(input_type): assert "instrument/detector/data" in entry_node dataset = entry_node["instrument/detector/data"] assert dataset.dtype == input_type + assert "control" not in entry_node + + +def test_machine_electric_current(): + """Test machine electric current is handle by the convertor""" + with tempfile.TemporaryDirectory() as root_dir: + bliss_mock = MockBlissAcquisition( + n_sample=1, + n_sequence=1, + n_scan_per_sequence=2, + n_darks=5, + n_flats=5, + with_nx_detector_attr=True, + output_dir=root_dir, + detector_name="my_detector", + ) + sample = bliss_mock.samples[0] + assert os.path.exists(sample.sample_file) + # append current to the bliss file + # from the example file I had it looks like this information can be saved at different location + # and can be either a number (for dark and flat for example) or a list (for projections) + with h5py.File(sample.sample_file, mode="a") as h5f: + node_names = ("2.1", "3.1") + machine_elec_current = (602, 589) # those are in ma + start_times = ( + "2022-01-15T21:07:58.360095+02:00", + "2022-01-15T21:07:59.360095+02:00", + ) + for (node_name, machine_elec_current_ma, st) in zip( + node_names, machine_elec_current, start_times + ): + h5f[f"{node_name}/instrument/machine/current"] = machine_elec_current_ma + h5f[f"{node_name}/instrument/machine/current"].attrs["unit"] = "mA" + h5f[node_name]["start_time"] = st + + assert "4.1" in h5f + assert "5.1" in h5f + assert ( + "6.1" not in h5f + ) # this is because n_scan_per_sequence == 2 in MockBlissAcquisition + + # create some X.2 for machine electric current as this is done in Bliss + for node_name in ("4.2", "5.2"): + h5f.require_group(node_name)["title"] = _BlissSample.get_title( + "projection" + ) + + current_monitor_dataset = h5f.require_dataset( + f"{node_name}/measurement/current", shape=(3), dtype=numpy.float32 + ) + current_monitor_dataset[:] = numpy.linspace( + 0.9, 0.96, 3, dtype=numpy.float32, endpoint=True + ) + current_monitor_dataset.attrs["unit"] = "A" + + # define start_time and end_time to insure conversion is correct + # start_time and end_time is required for both: + # * from X.1 to create frame time stamp + # * from X.2 to get machine electric current time stamp + start_times = ( + "2022-01-15T21:08:58.360095+02:00", + "2022-01-15T21:10:58.360095+02:00", + ) + end_times = ( + "2022-01-15T21:09:58.360095+02:00", + "2022-01-15T21:11:58.360095+02:00", + ) + tuple_node_names = (["4.1", "4.2"], ["5.1", "5.2"]) + + for (node_names, st, et) in zip(tuple_node_names, start_times, end_times): + for node_name in node_names: + h5f[node_name]["start_time"] = st + h5f[node_name]["end_time"] = et + h5f[node_name].require_group("instrument") + + # convert the file + config = TomoHDF5Config() + config.output_file = sample.sample_file.replace(".h5", ".nx") + config.single_file = True + config.request_input = False + config.raises_error = True + with pytest.raises(ValueError): + converter.from_h5_to_nx(configuration=config) + config.input_file = sample.sample_file + converter.from_h5_to_nx(configuration=config) + # insure only one file is generated + assert os.path.exists(config.output_file) + # insure data is here + nx_tomo = NXtomo().load(config.output_file, "entry0000") + expected_results = numpy.concatenate( + [ + [602 / 1000] * 10, + [589 / 1000] * 10, + numpy.linspace(0.9, 0.96, 10, dtype=numpy.float32), + numpy.linspace(0.9, 0.96, 10, dtype=numpy.float32), + ] + ) + n_frames = ( + 10 * 4 + ) # there is 10 frames per scan. One dark, one flat and two projections scans + assert len(nx_tomo.control.data.value) == n_frames + numpy.testing.assert_allclose( + nx_tomo.control.data.value, expected_results, rtol=0.001 + ) + + # test also from tomoscan + scan = HDF5TomoScan(scan=config.output_file, entry="entry0000") + # check getting the projections electric current + assert ( + len( + scan.electric_current[ + scan.image_key_control == ImageKey.PROJECTION.value + ] + ) + == 20 + ) + assert ( + len( + scan.electric_current[ + scan.image_key_control == ImageKey.DARK_FIELD.value + ] + ) + == 10 + ) + assert ( + len( + scan.electric_current[ + scan.image_key_control == ImageKey.FLAT_FIELD.value + ] + ) + == 10 + ) diff --git a/nxtomomill/converter/version.py b/nxtomomill/converter/version.py index bcbe5f87b4058be823a04d063a171fbf79b84303..1c45a555d0a115e4f86c98a14182aec0e9f06ff3 100644 --- a/nxtomomill/converter/version.py +++ b/nxtomomill/converter/version.py @@ -1,4 +1,4 @@ -LATEST_VERSION = 1.1 +LATEST_VERSION = 1.2 CURRENT_OUTPUT_VERSION = LATEST_VERSION diff --git a/nxtomomill/io/config/configbase.py b/nxtomomill/io/config/configbase.py index f95a5b42fc640c8ed1361c0ee1fd084d9fabef18..7db835696202e802c996f78981c7a9fbcaf19e13 100644 --- a/nxtomomill/io/config/configbase.py +++ b/nxtomomill/io/config/configbase.py @@ -51,6 +51,7 @@ class ConfigBase: self._file_extension = FileExtension.NX self._log_level = logging.WARNING self._field_of_view = None + self._machine_electric_current_keys = None def __setattr__(self, __name, __value): if self.__isfrozen and not hasattr(self, __name): @@ -175,6 +176,14 @@ class ConfigBase: raise TypeError("keys elmts are expected to be str") self._z_trans_keys = keys + @property + def machine_electric_current_keys(self) -> Iterable: + return self._machine_electric_current_keys + + @machine_electric_current_keys.setter + def machine_electric_current_keys(self, keys: Iterable) -> None: + self._machine_electric_current_keys = keys + def to_dict(self) -> dict: """convert the configuration to a dictionary""" raise NotImplementedError("Base class") diff --git a/nxtomomill/io/config/edfconfig.py b/nxtomomill/io/config/edfconfig.py index d8b895953afdeda59e63c633a03a5bbdf329f940..b32a053a12cb7208553e6f58d40467bcfc589020 100644 --- a/nxtomomill/io/config/edfconfig.py +++ b/nxtomomill/io/config/edfconfig.py @@ -308,6 +308,7 @@ class TomoEDFConfig(ConfigBase): self._y_trans_keys = Tomo.EDF.Y_TRANS self._z_trans_keys = Tomo.EDF.Z_TRANS self._rot_angle_keys = Tomo.EDF.ROT_ANGLE + self._machine_electric_current_keys = Tomo.EDF.MACHINE_ELECTRIC_CURRENT # dark and flat self._dark_names = Tomo.EDF.DARK_NAMES diff --git a/nxtomomill/io/config/hdf5config.py b/nxtomomill/io/config/hdf5config.py index 63b9143f089b1fcfb3bc9a9a516ec0ea421a5709..da9db61bee33bc0d3ec934397e34e9a7b1c628d4 100644 --- a/nxtomomill/io/config/hdf5config.py +++ b/nxtomomill/io/config/hdf5config.py @@ -335,6 +335,9 @@ class TomoHDF5Config(ConfigBase): self._diode_keys = settings.Tomo.H5.DIODE_KEYS self._expo_time_keys = settings.Tomo.H5.ACQ_EXPO_TIME_KEYS self._sample_detector_distance_keys = settings.Tomo.H5.DISTANCE_KEYS + self._machine_electric_current_keys = ( + settings.Tomo.H5.MACHINE_ELECTRIC_CURRENT_KEYS + ) # information regarding titles self._entries = None diff --git a/nxtomomill/nexus/nxdetector.py b/nxtomomill/nexus/nxdetector.py index 73571e343a38af4b86ebd7ca9337dd765a04771e..12401691aad2a9177be691949b3a1c1c7ab3f598 100644 --- a/nxtomomill/nexus/nxdetector.py +++ b/nxtomomill/nexus/nxdetector.py @@ -96,7 +96,7 @@ class NXdetector(NXobject): self._set_freeze(True) @property - def data(self) -> Union[numpy.ndarray, tuple]: + def data(self) -> Optional[Union[numpy.ndarray, tuple]]: """data can be None, a numpy array or a list of DataUrl xor h5py Virtual Source""" return self._data diff --git a/nxtomomill/nexus/nxmonitor.py b/nxtomomill/nexus/nxmonitor.py new file mode 100644 index 0000000000000000000000000000000000000000..b17895bec17d8446e3467ac9561f0e78f6ab519c --- /dev/null +++ b/nxtomomill/nexus/nxmonitor.py @@ -0,0 +1,123 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ + + +__authors__ = ["H. Payno"] +__license__ = "MIT" +__date__ = "02/05/2022" + + +from functools import partial +from operator import is_not +from typing import Optional, Union +import numpy +from nxtomomill.nexus.nxobject import NXobject +from silx.utils.proxy import docstring +from tomoscan.nexus.paths.nxtomo import get_paths as get_nexus_paths +from .utils import get_data_and_unit +from .nxobject import ElementWithUnit +from tomoscan.unitsystem import ElectricCurrentSystem + + +class NXmonitor(NXobject): + def __init__(self, node_name="control", parent: Optional[NXobject] = None) -> None: + super().__init__(node_name=node_name, parent=parent) + self._set_freeze(False) + self._data = ElementWithUnit(default_unit=ElectricCurrentSystem.AMPERE) + self._set_freeze(True) + + @property + def data(self) -> Optional[numpy.ndarray]: + """monitor data. + In the case of NXtomo it expects to contains machine electric current for each frame + """ + return self._data + + @data.setter + def data(self, data: Optional[Union[numpy.ndarray, list, tuple]]): + if isinstance(data, (tuple, list)): + if len(data) == 0: + data = None + else: + data = numpy.asarray(data) + + if isinstance(data, numpy.ndarray): + if not data.ndim == 1: + raise ValueError(f"data is expected to be 1D and not {data.ndim}d") + elif not isinstance(data, type(None)): + raise TypeError( + f"data is expected to be None or a numpy array. Not {type(data)}" + ) + self._data.value = data + + @docstring(NXobject) + def to_nx_dict( + self, + nexus_path_version: Optional[float] = None, + data_path: Optional[str] = None, + ) -> dict: + + nexus_paths = get_nexus_paths(nexus_path_version) + monitor_nexus_paths = nexus_paths.nx_monitor_paths + + nx_dict = {} + if self.data.value is not None: + if monitor_nexus_paths.DATA_PATH is not None: + data_path = f"{self.path}/{monitor_nexus_paths.DATA_PATH}" + nx_dict[data_path] = self.data.value + nx_dict["@".join([data_path, "unit"])] = str(self.data.unit) + + if nx_dict != {}: + nx_dict[f"{self.path}@NX_class"] = "NXmonitor" + return nx_dict + + def _load(self, file_path: str, data_path: str, nexus_version: float) -> NXobject: + """ + Create and load an NXmonitor from data on disk + """ + nexus_paths = get_nexus_paths(nexus_version) + monitor_nexus_paths = nexus_paths.nx_monitor_paths + if monitor_nexus_paths.DATA_PATH is not None: + self.data, self.data.unit = get_data_and_unit( + file_path=file_path, + data_path="/".join([data_path, monitor_nexus_paths.DATA_PATH]), + default_unit="Ampere", + ) + + @docstring(NXobject) + def concatenate(nx_objects: tuple, node_name: str = "control"): + # filter None obj + nx_objects = tuple(filter(partial(is_not, None), nx_objects)) + if len(nx_objects) == 0: + return None + nx_monitor = NXmonitor(node_name=node_name) + data = [ + nx_obj.data.value * nx_obj.data.unit.value + for nx_obj in nx_objects + if nx_obj.data.value is not None + ] + if len(data) > 0: + nx_monitor.data = numpy.concatenate(data) + return nx_monitor diff --git a/nxtomomill/nexus/nxtomo.py b/nxtomomill/nexus/nxtomo.py index f6867fc1f476aecbe4fbf595c71d618ccdf73ac5..583ac4592a6a894c63f2a5f26a3c5989d8a62679 100644 --- a/nxtomomill/nexus/nxtomo.py +++ b/nxtomomill/nexus/nxtomo.py @@ -34,6 +34,8 @@ import os from typing import Optional, Union from tomoscan.io import HDF5File from tomoscan.nexus.paths.nxtomo import LATEST_VERSION as LATEST_NXTOMO_VERSION + +from nxtomomill.nexus.nxmonitor import NXmonitor from .nxobject import NXobject, ElementWithUnit from .nxinstrument import NXinstrument from .nxsample import NXsample @@ -63,6 +65,7 @@ class NXtomo(NXobject): self._end_time = None self._instrument = NXinstrument(node_name="instrument", parent=self) self._sample = NXsample(node_name="sample", parent=self) + self._control = NXmonitor(node_name="control", parent=self) self._group_size = None self._energy = ElementWithUnit( default_unit=EnergySI.KILOELECTRONVOLT @@ -130,6 +133,18 @@ class NXtomo(NXobject): ) self._sample = sample + @property + def control(self) -> Optional[NXmonitor]: + return self._control + + @control.setter + def control(self, control: Optional[NXmonitor]) -> None: + if not isinstance(control, (type(None), NXmonitor)): + raise TypeError( + f"control is expected ot be an instance of {NXmonitor} or None. Not {type(control)}" + ) + self._control = control + @property def energy(self) -> Optional[float]: """ @@ -186,6 +201,13 @@ class NXtomo(NXobject): else: _logger.info("no instrument found. Won't be saved") + if self.control is not None: + nx_dict.update( + self.control.to_nx_dict(nexus_path_version=nexus_path_version) + ) + else: + _logger.info("no control found. Won't be saved") + if self.start_time is not None: path_start_time = f"{self.path}/{nexus_paths.START_TIME_PATH}" if isinstance(self.start_time, datetime): @@ -334,6 +356,9 @@ class NXtomo(NXobject): nexus_version=nexus_version, detector_data_as=detector_data_as, ) + self.control._load( + file_path, "/".join([data_path, "control"]), nexus_version=nexus_version + ) return self def concatenate(nx_objects: tuple, node_name=""): @@ -382,4 +407,9 @@ class NXtomo(NXobject): ) nx_tomo.instrument.parent = nx_tomo + nx_tomo.control = NXmonitor.concatenate( + tuple([nx_obj.control for nx_obj in nx_objects]), + ) + nx_tomo.control.parent = nx_tomo + return nx_tomo diff --git a/nxtomomill/nexus/test/test_nxmonitor.py b/nxtomomill/nexus/test/test_nxmonitor.py new file mode 100644 index 0000000000000000000000000000000000000000..fe0f74875423968993434045ed3c03f62cb37e8a --- /dev/null +++ b/nxtomomill/nexus/test/test_nxmonitor.py @@ -0,0 +1,68 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ + + +__authors__ = ["H. Payno"] +__license__ = "MIT" +__date__ = "02/05/2022" + + +from nxtomomill.nexus.nxmonitor import NXmonitor +from nxtomomill.nexus import concatenate +import pytest +import numpy + + +def test_nx_sample(): + """test creation and saving of an nxsource""" + nx_monitor = NXmonitor() + # check name + with pytest.raises(TypeError): + nx_monitor.data = 12 + with pytest.raises(ValueError): + nx_monitor.data = numpy.zeros([12, 12]) + nx_monitor.data = tuple() + nx_monitor.data = numpy.zeros(12) + + assert isinstance(nx_monitor.to_nx_dict(), dict) + + # test concatenate + nx_monitor_1 = NXmonitor() + nx_monitor_1.data = numpy.arange(10) + nx_monitor_2 = NXmonitor() + nx_monitor_2.data = numpy.arange(10)[::-1] + nx_monitor_2.data.unit = "mA" + + nx_monitor_concat = concatenate([nx_monitor_1, nx_monitor_2]) + assert isinstance(nx_monitor_concat, NXmonitor) + numpy.testing.assert_array_equal( + nx_monitor_concat.data.value, + numpy.concatenate( + [ + nx_monitor_1.data.value, + nx_monitor_2.data.value * 10e-4, + ] + ), + ) diff --git a/nxtomomill/settings.py b/nxtomomill/settings.py index 276b348cdf96e4d1b09f0909716d8def5b0c4829..94be932c5bdd10c282799cf73397a783deaef3d5 100644 --- a/nxtomomill/settings.py +++ b/nxtomomill/settings.py @@ -122,6 +122,9 @@ class Tomo: DISTANCE_KEYS = ("technique/scan/sample_detector_distance",) """keys used by bliss to store the sample to detector distance""" + MACHINE_ELECTRIC_CURRENT_KEYS = (("current"),) + """keys used by bliss to store the electric current""" + class EDF: """EDF settings for tomography""" @@ -140,6 +143,8 @@ class Tomo: Z_TRANS = ("sz",) + MACHINE_ELECTRIC_CURRENT = ("srcur", "srcurrent") + # EDF_TO_IGNORE = ['HST', '_slice_'] TO_IGNORE = ("_slice_",) diff --git a/nxtomomill/utils/utils.py b/nxtomomill/utils/utils.py index 30243765966887733ca51fd0dbf43bb13d42f285..d45e7db71b06309506c47795f6cb9078cc74f4a5 100644 --- a/nxtomomill/utils/utils.py +++ b/nxtomomill/utils/utils.py @@ -30,6 +30,7 @@ __date__ = "29/04/2019" import typing +from datetime import datetime import numpy import os from tomoscan.io import HDF5File @@ -461,3 +462,22 @@ def change_image_key_control( image_key_value = ImageKey.PROJECTION image_keys[frames_indexes] = image_key_value.value node[image_keys_path][:] = image_keys + + +def str_datetime_to_numpy_datetime64( + my_datetime: typing.Union[str, datetime] +) -> numpy.datetime64: + # numpy deprecates time zone awarness conversion to numpy.datetime64. + # so we remove the time zone info. + if isinstance(my_datetime, str): + datetime_as_datetime = datetime.fromisoformat(my_datetime) + elif isinstance(my_datetime, datetime): + datetime_as_datetime = my_datetime + else: + raise TypeError( + f"my_datetime is expected to be a str or an instance of datetime. Not {type(my_datetime)}" + ) + + datetime_as_utc_datetime = datetime_as_datetime.astimezone(None) + tz_free_datetime_as_datetime = datetime_as_utc_datetime.replace(tzinfo=None) + return numpy.datetime64(tz_free_datetime_as_datetime).astype("