From ea438ed3068c9d6b90d25bbdeac30bb523ad0d25 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Thu, 28 Apr 2022 15:20:19 +0200 Subject: [PATCH 01/23] current: add `deduce_electrical_current` function which allows to deduce current for a timestamp according to a data base and doing the interpolation between those --- .../hdf5/acquisition/test/test_utils.py | 118 ++++++++++++++++++ .../converter/hdf5/acquisition/utils.py | 62 +++++++++ 2 files changed, 180 insertions(+) create mode 100644 nxtomomill/converter/hdf5/acquisition/test/test_utils.py diff --git a/nxtomomill/converter/hdf5/acquisition/test/test_utils.py b/nxtomomill/converter/hdf5/acquisition/test/test_utils.py new file mode 100644 index 0000000..51cd7fd --- /dev/null +++ b/nxtomomill/converter/hdf5/acquisition/test/test_utils.py @@ -0,0 +1,118 @@ +# 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" + + +from datetime import datetime +import numpy +import pytest +from nxtomomill.converter.hdf5.acquisition.utils import deduce_electrical_current + + +def test_deduce_electrical_current(): + """ + Test `deduce_electrical_current` function. Base function to compute current for each frame according to it's timestamp + """ + + electrical_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_electrical_current(tuple(), {}) + with pytest.raises(TypeError): + deduce_electrical_current(12, 2) + with pytest.raises(TypeError): + deduce_electrical_current( + [ + 12, + ], + 2, + ) + with pytest.raises(TypeError): + deduce_electrical_current( + [ + 2, + ], + electrical_current_datetimes, + ) + + converted_electrical_currents = {} + for elec_cur_datetime_str, elect_cur in electrical_current_datetimes.items(): + datetime_as_datetime = datetime.fromisoformat(elec_cur_datetime_str) + assert isinstance(datetime_as_datetime, datetime) + converted_electrical_currents[datetime_as_datetime] = elect_cur + + # check exacts values, left and right bounds + assert deduce_electrical_current( + (datetime.fromisoformat("2022-01-15T21:07:58.360095+02:00"),), + converted_electrical_currents, + ) == (1.1,) + assert deduce_electrical_current( + (datetime.fromisoformat("2022-01-14T21:07:58.360095+02:00"),), + converted_electrical_currents, + ) == (1.1,) + assert deduce_electrical_current( + (datetime.fromisoformat("2022-12-15T21:07:58.360095+02:00"),), + converted_electrical_currents, + ) == (1000.3,) + assert deduce_electrical_current( + (datetime.fromisoformat("2022-12-16T21:07:58.360095+02:00"),), + converted_electrical_currents, + ) == (1000.3,) + # check interpolated values + numpy.testing.assert_almost_equal( + deduce_electrical_current( + (datetime.fromisoformat("2022-04-15T21:08:58.360095+02:00"),), + converted_electrical_currents, + )[0], + (122.2,), + ) + numpy.testing.assert_almost_equal( + deduce_electrical_current( + (datetime.fromisoformat("2022-04-15T21:10:28.360095+02:00"),), + converted_electrical_currents, + )[0], + (223.3,), + ) + + # test several call and insure keep order + numpy.testing.assert_almost_equal( + deduce_electrical_current( + ( + datetime.fromisoformat("2022-01-15T21:07:58.360095+02:00"), + datetime.fromisoformat("2022-04-15T21:10:28.360095+02:00"), + datetime.fromisoformat("2022-04-15T21:08:58.360095+02:00"), + ), + converted_electrical_currents, + ), + (1.1, 223.3, 122.2), + ) diff --git a/nxtomomill/converter/hdf5/acquisition/utils.py b/nxtomomill/converter/hdf5/acquisition/utils.py index 0db64e5..70373f7 100644 --- a/nxtomomill/converter/hdf5/acquisition/utils.py +++ b/nxtomomill/converter/hdf5/acquisition/utils.py @@ -28,11 +28,16 @@ Utils related to bliss-HDF5 """ +from datetime import datetime +from argon2 import Type + +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 +148,60 @@ def guess_nx_detector(node: h5py.Group) -> tuple: nx_detectors = sorted(nx_detectors, key=lambda det: det.name) return tuple(nx_detectors) + + +def deduce_electrical_current( + timestamps: tuple, knowned_electrical_current: dict +) -> dict: + """ + :param dict electrical_current_points: keys are electrical timestamp. Value is electrical current + :param tuple timestamp: keys are frame index. timestamp. Value is electrical current + """ + if not isinstance(knowned_electrical_current, dict): + raise TypeError("knowned_electrical_current is expected to be a dict") + if len(knowned_electrical_current) == 0: + raise ValueError( + "knowned_electrical_current should at least contains one element" + ) + for key, value in knowned_electrical_current.items(): + if not isinstance(key, datetime): + raise TypeError( + f"knowned_electrical_current keys are expected to be instances of {datetime} and not {type(key)}" + ) + if not isinstance(value, (float, numpy.number)): + raise TypeError( + "knowned_electrical_current values are expected to be instances of float" + ) + + know_timestamps = sorted(knowned_electrical_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, datetime): + raise TypeError( + f"elements of timestamps are expected to be instances of {datetime}" + ) + (ts1, w1), (ts2, w2) = get_closest_timestamps(timestamp) + ec1 = knowned_electrical_current.get(ts1) + assert ec1 is not None + ec2 = knowned_electrical_current.get(ts2, 0.0) + res.append(ec1 * w1 + ec2 * w2) + return tuple(res) -- GitLab From 5733bed1bbf535daaf9fe34e7b95be7ec58863cb Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 2 May 2022 11:28:31 +0200 Subject: [PATCH 02/23] nexus: add nxmonitor classes --- nxtomomill/nexus/nxmonitor.py | 100 ++++++++++++++++++++++++ nxtomomill/nexus/test/test_nxmonitor.py | 46 +++++++++++ 2 files changed, 146 insertions(+) create mode 100644 nxtomomill/nexus/nxmonitor.py create mode 100644 nxtomomill/nexus/test/test_nxmonitor.py diff --git a/nxtomomill/nexus/nxmonitor.py b/nxtomomill/nexus/nxmonitor.py new file mode 100644 index 0000000..6d8840e --- /dev/null +++ b/nxtomomill/nexus/nxmonitor.py @@ -0,0 +1,100 @@ +# 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 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]: + """data can be None, a numpy array or a list of DataUrl xor h5py Virtual Source""" + 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) + + nx_dict = {} + if self.data.value is not None: + data_path = f"{self.path}/{nexus_paths.ELECTRIC_CURRENT_PATH}" + nx_dict[data_path] = self.data.value + nx_dict["@".join([data_path, "unit"])] = 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) + + self.data, self.data.unit = get_data_and_unit( + file_path=file_path, + data_path="/".join([data_path, nexus_paths.ELECTRIC_CURRENT_PATH]), + default_unit="Ampere", + ) diff --git a/nxtomomill/nexus/test/test_nxmonitor.py b/nxtomomill/nexus/test/test_nxmonitor.py new file mode 100644 index 0000000..56e7c10 --- /dev/null +++ b/nxtomomill/nexus/test/test_nxmonitor.py @@ -0,0 +1,46 @@ +# 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 +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 + nx_monitor.data = numpy.zeros([12, 12]) + nx_monitor.data = numpy.zeros(12) + + assert isinstance(nx_monitor.to_nx_dict(), dict) -- GitLab From 5e6ec65226059e38acdc459b74aef2edb61df578 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 2 May 2022 15:20:08 +0200 Subject: [PATCH 03/23] fix 9343b50821f538a7d360dc316a617a5a24b2d43a --- nxtomomill/nexus/test/test_nxmonitor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxtomomill/nexus/test/test_nxmonitor.py b/nxtomomill/nexus/test/test_nxmonitor.py index 56e7c10..746ed7c 100644 --- a/nxtomomill/nexus/test/test_nxmonitor.py +++ b/nxtomomill/nexus/test/test_nxmonitor.py @@ -40,7 +40,9 @@ def test_nx_sample(): # check name with pytest.raises(TypeError): nx_monitor.data = 12 + with pytest.raises(TypeError): 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) -- GitLab From a2194cc8df5064c7586e348ba0efe137720b9211 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Tue, 10 May 2022 08:42:11 +0200 Subject: [PATCH 04/23] add machine electric current handling # Conflicts: # nxtomomill/nexus/nxtomo.py --- nxtomomill/converter/edf/edfconverter.py | 22 ++- .../hdf5/acquisition/baseacquisition.py | 66 ++++++- .../hdf5/acquisition/standardacquisition.py | 181 ++++++++++++++++-- .../hdf5/acquisition/test/test_utils.py | 52 ++--- .../converter/hdf5/acquisition/utils.py | 30 ++- nxtomomill/converter/hdf5/hdf5converter.py | 2 + .../converter/hdf5/test/test_hdf5converter.py | 112 ++++++++++- nxtomomill/converter/version.py | 2 +- nxtomomill/io/config/hdf5config.py | 11 ++ nxtomomill/nexus/nxdetector.py | 2 +- nxtomomill/nexus/nxmonitor.py | 25 ++- nxtomomill/nexus/nxtomo.py | 25 +++ nxtomomill/settings.py | 3 + 13 files changed, 455 insertions(+), 78 deletions(-) diff --git a/nxtomomill/converter/edf/edfconverter.py b/nxtomomill/converter/edf/edfconverter.py index 2fcc564..b0b9815 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 @@ -736,6 +737,25 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: if source_grp is not None and "NX_class" not in source_grp.attrs: source_grp.attrs["NX_class"] = "NXsource" + # try to create the current dataset + electric_current = scan.retrieve_information( + scan=scan.path, + dataset_basename=scan.path, + ref_file=None, + key="SrCurrent", + key_aliases=["SRCUR", "machineCurrentStart"], + type_=float, + scan_info=scan.scan_info, + ) + nexus_paths = get_nexus_paths(LATEST_VERSION) + if electric_current is not None: + electric_current = h5d["/entry"].create_dataset( + nexus_paths.ELECTRIC_CURRENT_PATH, + shape=(n_frames,), + dtype=numpy.float32, + ) + electric_current[:] = electric_current + h5d.flush() for i_meta, meta in enumerate(metadata): diff --git a/nxtomomill/converter/hdf5/acquisition/baseacquisition.py b/nxtomomill/converter/hdf5/acquisition/baseacquisition.py index e417925..6a5773c 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,48 @@ 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 special case: elec_current is sometime a list and sometime a number... + 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 67646d8..5008425 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -34,11 +34,12 @@ __license__ = "MIT" __date__ = "14/02/2022" +from datetime import datetime 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 +48,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 +101,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._know_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 +137,10 @@ class StandardAcquisition(BaseAcquisition): def n_frames(self): return self._n_frames + @property + def n_frames_current_bliss_scan(self): + return self._n_frames_current_bliss_scan + @property def dim_1(self): return self._dim_1 @@ -152,6 +157,13 @@ class StandardAcquisition(BaseAcquisition): def expo_time(self): return self._acq_expo_time + @property + def know_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._know_machine_electric_current + @property def is_xrd_ct(self): return False @@ -266,6 +278,7 @@ class StandardAcquisition(BaseAcquisition): n_frame = shape[0] self._n_frames += n_frame + self._n_frames_current_bliss_scan = n_frame if self.dim_1 is None: self._dim_2 = shape[1] self._dim_1 = shape[2] @@ -306,7 +319,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 +430,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 +442,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 +469,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 +477,109 @@ class StandardAcquisition(BaseAcquisition): entry_path=entry_path, entry_url=entry_url, ) + has_frames = True + # try to get some other metadata + + from pandas import date_range + + # 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) + + def register_frame_timestamp(): + if start_time is None or end_time is None: + if start_time != end_time: + message = f"Unable to find start_time and / or end_time. Takes {start_time or end_time} as frame time stamp for {entry} " + self._frames_timestamp.extend( + [start_time or end_time] * self._n_frames_current_bliss_scan + ) + _logger.warning(message) + else: + message = f"Unable to find start_time and end_time. Can't deduce frames time stamp for {entry}" + _logger.error(message) + else: + self._frames_timestamp.extend( + date_range( + start=start_time, + end=end_time, + periods=self._n_frames_current_bliss_scan, + ) + ) + + if has_frames: + register_frame_timestamp() + + # handle electric current + def register_machine_electric_current(): + ( + 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 = ( + electric_current_unit_ref.value + * ElectricCurrentSystem.from_str(electric_current_unit).value + ) + + new_know_electric_currents = {} + if start_time is None or end_time is None: + _logger.warning( + "Unable to find start_time and / or end_time. Will pick the first available electricl_current for the frame" + ) + if start_time != end_time: + t_time = start_time or end_time + # if at least one can find out + new_know_electric_currents[t_time] = ( + electric_currents[0] * unit_factor + ) + 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[start_time] = ( + electric_currents[0] * unit_factor + ) + new_know_electric_currents[end_time] = ( + electric_currents[0] * unit_factor + ) + else: + # TODO: might want to skip dependancy on pandas + timestamps = date_range( + start=start_time, + end=end_time, + periods=len(electric_currents), + ) + for timestamp, mach_electric_current in zip( + timestamps, electric_currents + ): + new_know_electric_currents[timestamp] = ( + mach_electric_current * unit_factor + ) + self._know_machine_electric_current.update( + new_know_electric_currents + ) + + register_machine_electric_current() def _preprocess_registered_entries(self): """parse all frames of the different steps and retrieve data, image_key...""" self._n_frames = 0 + self._n_frames_current_bliss_scan = 0 + # number of frame contains in X.1 self._dim_1 = None self._dim_2 = None self._data_type = None @@ -474,6 +588,8 @@ class StandardAcquisition(BaseAcquisition): self._z_translation = [] self._image_key_control = [] self._rotation_angle = [] + self._know_machine_electric_current = {} + self._frames_timestamp = [] self._virtual_sources = [] self._virtual_sources_len = [] self._diode = [] @@ -533,21 +649,29 @@ class StandardAcquisition(BaseAcquisition): res[alias] = self.configuration.param_already_defined[alias] return res - def _generic_path_getter(self, path, message, level="warning"): + def _generic_path_getter(self, path, message, level="warning", entry=None): """ :param str level: level can be logging.level values : "warning", "error", "info" + :param H5group entry: user can provide directly an entry to be used as an open h5Group """ if self.root_url is None: return None self._check_has_metadata() - with self.read_entry() as entry: - if path in entry: - return h5py_read_dataset(entry[path]) + + def process(h5_group): + if path in h5_group: + return h5py_read_dataset(h5_group[path]) else: if message is not None: getattr(_logger, level)(message) return None + if entry is None: + with self.read_entry() as h5_group: + return process(h5_group) + else: + return process(entry) + def _get_source_name(self): """ """ return self._generic_path_getter( @@ -604,16 +728,20 @@ class StandardAcquisition(BaseAcquisition): message="unable to find information regarding tomo_n", ) - def _get_start_time(self): + def _get_start_time(self, entry=None): return self._generic_path_getter( path=self._START_TIME_PATH, message="Unable to find start time", level="info", + entry=entry, ) - def _get_end_time(self): + def _get_end_time(self, entry=None): return self._generic_path_getter( - path=self._END_TIME_PATH, message="Unable to find end time", level="info" + path=self._END_TIME_PATH, + message="Unable to find end time", + level="info", + entry=entry, ) def _get_energy(self, ask_if_0, input_callback): @@ -760,7 +888,7 @@ class StandardAcquisition(BaseAcquisition): def to_NXtomos(self, request_input, input_callback, check_tomo_n=True) -> tuple: self._preprocess_registered_entries() - nx_tomo = NXtomo("/") + nx_tomo = NXtomo() # 1. root level information # start and end time @@ -834,7 +962,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 +977,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 +987,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.know_machine_electric_current not in (None, dict()): + nx_tomo.control.data = deduce_machine_electric_current( + timestamps=self._frames_timestamp, + knowned_machine_electric_current=self._know_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_utils.py b/nxtomomill/converter/hdf5/acquisition/test/test_utils.py index 51cd7fd..5be2658 100644 --- a/nxtomomill/converter/hdf5/acquisition/test/test_utils.py +++ b/nxtomomill/converter/hdf5/acquisition/test/test_utils.py @@ -31,15 +31,15 @@ __date__ = "28/04/2022" from datetime import datetime import numpy import pytest -from nxtomomill.converter.hdf5.acquisition.utils import deduce_electrical_current +from nxtomomill.converter.hdf5.acquisition.utils import deduce_machine_electric_current -def test_deduce_electrical_current(): +def test_deduce_machine_electric_current(): """ - Test `deduce_electrical_current` function. Base function to compute current for each frame according to it's timestamp + Test `deduce_electric_current` function. Base function to compute current for each frame according to it's timestamp """ - electrical_current_datetimes = { + 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, @@ -47,72 +47,72 @@ def test_deduce_electrical_current(): "2022-12-15T21:07:58.360095+02:00": 1000.3, } with pytest.raises(ValueError): - deduce_electrical_current(tuple(), {}) + deduce_machine_electric_current(tuple(), {}) with pytest.raises(TypeError): - deduce_electrical_current(12, 2) + deduce_machine_electric_current(12, 2) with pytest.raises(TypeError): - deduce_electrical_current( + deduce_machine_electric_current( [ 12, ], 2, ) with pytest.raises(TypeError): - deduce_electrical_current( + deduce_machine_electric_current( [ 2, ], - electrical_current_datetimes, + electric_current_datetimes, ) - converted_electrical_currents = {} - for elec_cur_datetime_str, elect_cur in electrical_current_datetimes.items(): + converted_electric_currents = {} + for elec_cur_datetime_str, elect_cur in electric_current_datetimes.items(): datetime_as_datetime = datetime.fromisoformat(elec_cur_datetime_str) assert isinstance(datetime_as_datetime, datetime) - converted_electrical_currents[datetime_as_datetime] = elect_cur + converted_electric_currents[datetime_as_datetime] = elect_cur # check exacts values, left and right bounds - assert deduce_electrical_current( + assert deduce_machine_electric_current( (datetime.fromisoformat("2022-01-15T21:07:58.360095+02:00"),), - converted_electrical_currents, + converted_electric_currents, ) == (1.1,) - assert deduce_electrical_current( + assert deduce_machine_electric_current( (datetime.fromisoformat("2022-01-14T21:07:58.360095+02:00"),), - converted_electrical_currents, + converted_electric_currents, ) == (1.1,) - assert deduce_electrical_current( + assert deduce_machine_electric_current( (datetime.fromisoformat("2022-12-15T21:07:58.360095+02:00"),), - converted_electrical_currents, + converted_electric_currents, ) == (1000.3,) - assert deduce_electrical_current( + assert deduce_machine_electric_current( (datetime.fromisoformat("2022-12-16T21:07:58.360095+02:00"),), - converted_electrical_currents, + converted_electric_currents, ) == (1000.3,) # check interpolated values numpy.testing.assert_almost_equal( - deduce_electrical_current( + deduce_machine_electric_current( (datetime.fromisoformat("2022-04-15T21:08:58.360095+02:00"),), - converted_electrical_currents, + converted_electric_currents, )[0], (122.2,), ) numpy.testing.assert_almost_equal( - deduce_electrical_current( + deduce_machine_electric_current( (datetime.fromisoformat("2022-04-15T21:10:28.360095+02:00"),), - converted_electrical_currents, + converted_electric_currents, )[0], (223.3,), ) # test several call and insure keep order numpy.testing.assert_almost_equal( - deduce_electrical_current( + deduce_machine_electric_current( ( datetime.fromisoformat("2022-01-15T21:07:58.360095+02:00"), datetime.fromisoformat("2022-04-15T21:10:28.360095+02:00"), datetime.fromisoformat("2022-04-15T21:08:58.360095+02:00"), ), - converted_electrical_currents, + 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 70373f7..597bd6e 100644 --- a/nxtomomill/converter/hdf5/acquisition/utils.py +++ b/nxtomomill/converter/hdf5/acquisition/utils.py @@ -29,8 +29,6 @@ Utils related to bliss-HDF5 from datetime import datetime -from argon2 import Type - import numpy from nxtomomill.io.acquisitionstep import AcquisitionStep from silx.io.utils import h5py_read_dataset @@ -150,30 +148,30 @@ def guess_nx_detector(node: h5py.Group) -> tuple: return tuple(nx_detectors) -def deduce_electrical_current( - timestamps: tuple, knowned_electrical_current: dict +def deduce_machine_electric_current( + timestamps: tuple, knowned_machine_electric_current: dict ) -> dict: """ - :param dict electrical_current_points: keys are electrical timestamp. Value is electrical current - :param tuple timestamp: keys are frame index. timestamp. Value is electrical current + :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_electrical_current, dict): - raise TypeError("knowned_electrical_current is expected to be a dict") - if len(knowned_electrical_current) == 0: + if not isinstance(knowned_machine_electric_current, dict): + raise TypeError("knowned_machine_electric_current is expected to be a dict") + if len(knowned_machine_electric_current) == 0: raise ValueError( - "knowned_electrical_current should at least contains one element" + "knowned_machine_electric_current should at least contains one element" ) - for key, value in knowned_electrical_current.items(): + for key, value in knowned_machine_electric_current.items(): if not isinstance(key, datetime): raise TypeError( - f"knowned_electrical_current keys are expected to be instances of {datetime} and not {type(key)}" + f"knowned_machine_electric_current keys are expected to be instances of {datetime} and not {type(key)}" ) if not isinstance(value, (float, numpy.number)): raise TypeError( - "knowned_electrical_current values are expected to be instances of float" + "knowned_machine_electric_current values are expected to be instances of float" ) - know_timestamps = sorted(knowned_electrical_current.keys()) + know_timestamps = sorted(knowned_machine_electric_current.keys()) def get_closest_timestamps(timestamp) -> tuple: if timestamp in know_timestamps: @@ -200,8 +198,8 @@ def deduce_electrical_current( f"elements of timestamps are expected to be instances of {datetime}" ) (ts1, w1), (ts2, w2) = get_closest_timestamps(timestamp) - ec1 = knowned_electrical_current.get(ts1) + ec1 = knowned_machine_electric_current.get(ts1) assert ec1 is not None - ec2 = knowned_electrical_current.get(ts2, 0.0) + 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 5173f50..de608f5 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 18afe23..5dcfc8c 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,6 +40,7 @@ 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 @@ -1005,3 +1006,112 @@ 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", + ) + end_times = ("2022-01-15T21:07:59.170095+02:00", None) + for (node_name, machine_elec_current_ma, st, et) in zip( + node_names, machine_elec_current, start_times, end_times + ): + h5f[f"{node_name}/instrument/machine/current"] = machine_elec_current_ma + h5f[f"{node_name}/instrument/machine/current"].attrs["unit"] = "mA" + if st is not None: + h5f[node_name]["start_time"] = st + if et is not None: + h5f[node_name]["end_time"] = et + + 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, + ) diff --git a/nxtomomill/converter/version.py b/nxtomomill/converter/version.py index bcbe5f8..1c45a55 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/hdf5config.py b/nxtomomill/io/config/hdf5config.py index 63b9143..969cfac 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 @@ -674,6 +677,14 @@ class TomoHDF5Config(ConfigBase): else: self._sample_detector_distance_keys = paths + @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 + # frame type definition def _parse_frame_urls(self, urls: tuple): diff --git a/nxtomomill/nexus/nxdetector.py b/nxtomomill/nexus/nxdetector.py index 73571e3..1240169 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 index 6d8840e..f29d494 100644 --- a/nxtomomill/nexus/nxmonitor.py +++ b/nxtomomill/nexus/nxmonitor.py @@ -48,7 +48,9 @@ class NXmonitor(NXobject): @property def data(self) -> Optional[numpy.ndarray]: - """data can be None, a numpy array or a list of DataUrl xor h5py Virtual Source""" + """monitor data. + In the case of NXtomo it expects to contains machine electric current for each frame + """ return self._data @data.setter @@ -76,12 +78,14 @@ class NXmonitor(NXobject): ) -> 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: - data_path = f"{self.path}/{nexus_paths.ELECTRIC_CURRENT_PATH}" - nx_dict[data_path] = self.data.value - nx_dict["@".join([data_path, "unit"])] = self.data.unit + 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" @@ -92,9 +96,10 @@ class NXmonitor(NXobject): Create and load an NXmonitor from data on disk """ nexus_paths = get_nexus_paths(nexus_version) - - self.data, self.data.unit = get_data_and_unit( - file_path=file_path, - data_path="/".join([data_path, nexus_paths.ELECTRIC_CURRENT_PATH]), - default_unit="Ampere", - ) + 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", + ) diff --git a/nxtomomill/nexus/nxtomo.py b/nxtomomill/nexus/nxtomo.py index f6867fc..90200f3 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), NXsample)): + 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=""): diff --git a/nxtomomill/settings.py b/nxtomomill/settings.py index 276b348..4b0bd9a 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""" -- GitLab From f54a3d4fbf9537c55ff79b05f906269692061bfa Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 2 May 2022 15:12:15 +0200 Subject: [PATCH 05/23] rename test file for getting code coverage --- .../acquisition/test/{test_utils.py => test_acquisition_utils.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nxtomomill/converter/hdf5/acquisition/test/{test_utils.py => test_acquisition_utils.py} (100%) diff --git a/nxtomomill/converter/hdf5/acquisition/test/test_utils.py b/nxtomomill/converter/hdf5/acquisition/test/test_acquisition_utils.py similarity index 100% rename from nxtomomill/converter/hdf5/acquisition/test/test_utils.py rename to nxtomomill/converter/hdf5/acquisition/test/test_acquisition_utils.py -- GitLab From 0df5b08d059d422af90505756bcd38e0aaf69130 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 2 May 2022 15:21:09 +0200 Subject: [PATCH 06/23] standardacquisition: rename to --- .../hdf5/acquisition/standardacquisition.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index 5008425..db7b5b0 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -105,7 +105,7 @@ class StandardAcquisition(BaseAcquisition): 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._know_machine_electric_current = 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 @@ -158,11 +158,11 @@ class StandardAcquisition(BaseAcquisition): return self._acq_expo_time @property - def know_machine_electric_current(self) -> Optional[dict]: + 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._know_machine_electric_current + return self._known_machine_electric_current @property def is_xrd_ct(self): @@ -568,7 +568,7 @@ class StandardAcquisition(BaseAcquisition): new_know_electric_currents[timestamp] = ( mach_electric_current * unit_factor ) - self._know_machine_electric_current.update( + self._known_machine_electric_current.update( new_know_electric_currents ) @@ -588,7 +588,7 @@ class StandardAcquisition(BaseAcquisition): self._z_translation = [] self._image_key_control = [] self._rotation_angle = [] - self._know_machine_electric_current = {} + self._known_machine_electric_current = {} self._frames_timestamp = [] self._virtual_sources = [] self._virtual_sources_len = [] @@ -988,10 +988,10 @@ class StandardAcquisition(BaseAcquisition): nx_tomo.sample.z_translation.unit = "m" # 6. define control - if self.know_machine_electric_current not in (None, dict()): + 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._know_machine_electric_current, + knowned_machine_electric_current=self._known_machine_electric_current, ) nx_tomo.control.data.unit = ElectricCurrentSystem.AMPERE types = set() -- GitLab From 4d4d53c23915fccf64a5f8d663ffb4c9b0786b9b Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 2 May 2022 15:26:51 +0200 Subject: [PATCH 07/23] standardacquisition: move `register_frame_timestamp` and `_register_machine_electric_current` defined at `_preprocess_registered_entry` function level to the upper class level --- .../hdf5/acquisition/standardacquisition.py | 163 +++++++++--------- 1 file changed, 83 insertions(+), 80 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index db7b5b0..f8f5271 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -49,6 +49,7 @@ import h5py from silx.io.url import DataUrl from typing import Optional, Union from tomoscan.unitsystem import ElectricCurrentSystem +from pandas import date_range try: import hdf5plugin # noqa F401 @@ -480,8 +481,6 @@ class StandardAcquisition(BaseAcquisition): has_frames = True # try to get some other metadata - from pandas import date_range - # handle frame time stamp start_time = self._get_start_time(entry) if start_time is not None: @@ -490,89 +489,93 @@ class StandardAcquisition(BaseAcquisition): if end_time is not None: end_time = datetime.fromisoformat(end_time) - def register_frame_timestamp(): - if start_time is None or end_time is None: - if start_time != end_time: - message = f"Unable to find start_time and / or end_time. Takes {start_time or end_time} as frame time stamp for {entry} " - self._frames_timestamp.extend( - [start_time or end_time] * self._n_frames_current_bliss_scan - ) - _logger.warning(message) - else: - message = f"Unable to find start_time and end_time. Can't deduce frames time stamp for {entry}" - _logger.error(message) - else: - self._frames_timestamp.extend( - date_range( - start=start_time, - end=end_time, - periods=self._n_frames_current_bliss_scan, - ) - ) - if has_frames: - register_frame_timestamp() - - # handle electric current - def register_machine_electric_current(): - ( - 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." - ) + self._register_frame_timestamp(entry, start_time, end_time) - unit_factor = ( - electric_current_unit_ref.value - * ElectricCurrentSystem.from_str(electric_current_unit).value - ) + # 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) - new_know_electric_currents = {} - if start_time is None or end_time is None: - _logger.warning( - "Unable to find start_time and / or end_time. Will pick the first available electricl_current for the frame" - ) - if start_time != end_time: - t_time = start_time or end_time - # if at least one can find out - new_know_electric_currents[t_time] = ( - electric_currents[0] * unit_factor - ) - 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[start_time] = ( - electric_currents[0] * unit_factor - ) - new_know_electric_currents[end_time] = ( - electric_currents[0] * unit_factor - ) - else: - # TODO: might want to skip dependancy on pandas - timestamps = date_range( - start=start_time, - end=end_time, - periods=len(electric_currents), - ) - for timestamp, mach_electric_current in zip( - timestamps, electric_currents - ): - new_know_electric_currents[timestamp] = ( - mach_electric_current * unit_factor - ) - self._known_machine_electric_current.update( - new_know_electric_currents + 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 = ( + electric_current_unit_ref.value + * ElectricCurrentSystem.from_str(electric_current_unit).value + ) + + new_know_electric_currents = {} + if start_time is None or end_time is None: + _logger.warning( + "Unable to find start_time and / or end_time. Will pick the first available electricl_current for the frame" + ) + if start_time != end_time: + t_time = start_time or end_time + # if at least one can find out + new_know_electric_currents[t_time] = ( + electric_currents[0] * unit_factor ) + 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[start_time] = ( + electric_currents[0] * unit_factor + ) + new_know_electric_currents[end_time] = ( + electric_currents[0] * unit_factor + ) + else: + # TODO: might want to skip dependancy on pandas + timestamps = date_range( + start=start_time, + end=end_time, + periods=len(electric_currents), + ) + for timestamp, mach_electric_current in zip( + timestamps, electric_currents + ): + new_know_electric_currents[timestamp] = ( + mach_electric_current * unit_factor + ) + self._known_machine_electric_current.update(new_know_electric_currents) - register_machine_electric_current() + def _register_frame_timestamp(self, entry: h5py.Group, start_time, end_time): + """ + update frame time stamp for the provided entry (bliss scan) + """ + if start_time is None or end_time is None: + if start_time != end_time: + message = f"Unable to find start_time and / or end_time. Takes {start_time or end_time} as frame time stamp for {entry} " + self._frames_timestamp.extend( + [start_time or end_time] * self._n_frames_current_bliss_scan + ) + _logger.warning(message) + else: + message = f"Unable to find start_time and end_time. Can't deduce frames time stamp for {entry}" + _logger.error(message) + else: + self._frames_timestamp.extend( + date_range( + start=start_time, + end=end_time, + periods=self._n_frames_current_bliss_scan, + ) + ) def _preprocess_registered_entries(self): """parse all frames of the different steps and retrieve data, -- GitLab From 210d6f39b33e87780f78f6249dc46b33e6b9d407 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 2 May 2022 16:48:09 +0200 Subject: [PATCH 08/23] h52nx: handle machine electric current: get rid of the pandas dependency --- .../hdf5/acquisition/standardacquisition.py | 55 +++++++++++-------- .../test/test_acquisition_utils.py | 23 ++++---- .../converter/hdf5/acquisition/utils.py | 14 +++-- .../converter/hdf5/test/test_hdf5converter.py | 3 +- nxtomomill/nexus/test/test_nxmonitor.py | 2 +- nxtomomill/utils/utils.py | 20 +++++++ 6 files changed, 73 insertions(+), 44 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index f8f5271..0fc9af3 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -35,6 +35,8 @@ __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 @@ -49,7 +51,6 @@ import h5py from silx.io.url import DataUrl from typing import Optional, Union from tomoscan.unitsystem import ElectricCurrentSystem -from pandas import date_range try: import hdf5plugin # noqa F401 @@ -525,31 +526,32 @@ class StandardAcquisition(BaseAcquisition): if start_time != end_time: t_time = start_time or end_time # if at least one can find out - new_know_electric_currents[t_time] = ( - electric_currents[0] * unit_factor - ) + new_know_electric_currents[ + str_datetime_to_numpy_datetime64(t_time) + ] = (electric_currents[0] * unit_factor) 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[start_time] = ( - electric_currents[0] * unit_factor - ) - new_know_electric_currents[end_time] = ( - electric_currents[0] * unit_factor - ) + new_know_electric_currents[ + str_datetime_to_numpy_datetime64(start_time) + ] = (electric_currents[0] * unit_factor) + new_know_electric_currents[ + str_datetime_to_numpy_datetime64(end_time) + ] = (electric_currents[0] * unit_factor) else: - # TODO: might want to skip dependancy on pandas - timestamps = date_range( - start=start_time, - end=end_time, - periods=len(electric_currents), + timestamps = numpy.linspace( + start=str_datetime_to_numpy_datetime64(start_time).astype("f8"), + stop=str_datetime_to_numpy_datetime64(end_time).astype("f8"), + num=len(electric_currents), + endpoint=True, + dtype=" 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(" Date: Mon, 2 May 2022 17:13:51 +0200 Subject: [PATCH 09/23] hdf5converter: add some test within the tomoscan.esrf.HDF5TomoScan class --- .../converter/hdf5/test/test_hdf5converter.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/nxtomomill/converter/hdf5/test/test_hdf5converter.py b/nxtomomill/converter/hdf5/test/test_hdf5converter.py index 2f135df..8bfe0ce 100644 --- a/nxtomomill/converter/hdf5/test/test_hdf5converter.py +++ b/nxtomomill/converter/hdf5/test/test_hdf5converter.py @@ -47,6 +47,7 @@ try: 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 @@ -1114,3 +1115,31 @@ def test_machine_electric_current(): 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 + ) -- GitLab From cfe535ff55d5f2483586f2ce9e10f19829c8a78c Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 2 May 2022 17:14:10 +0200 Subject: [PATCH 10/23] ci: move CI to use `add_current` branch --- .gitlab-ci.yml | 4 +++- ci/install_scripts.sh | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6832114..1045178 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 ed3301e..81e7095 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(){ -- GitLab From a8c12cf8b57a2b24d5d0dc5d24db6c355d0c4402 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 9 May 2022 08:09:56 +0200 Subject: [PATCH 11/23] rename 'n_frames_current_bliss_scan' to 'n_frames_actual_bliss_scan' --- .../hdf5/acquisition/standardacquisition.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index 0fc9af3..6c50323 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -140,8 +140,8 @@ class StandardAcquisition(BaseAcquisition): return self._n_frames @property - def n_frames_current_bliss_scan(self): - return self._n_frames_current_bliss_scan + def n_frames_actual_bliss_scan(self): + return self._n_frames_actual_bliss_scan @property def dim_1(self): @@ -280,7 +280,7 @@ class StandardAcquisition(BaseAcquisition): n_frame = shape[0] self._n_frames += n_frame - self._n_frames_current_bliss_scan = 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] @@ -565,7 +565,7 @@ class StandardAcquisition(BaseAcquisition): t_time = str_datetime_to_numpy_datetime64(start_time or end_time) message = f"Unable to find start_time and / or end_time. Takes {t_time} as frame time stamp for {entry} " self._frames_timestamp.extend( - [t_time] * self._n_frames_current_bliss_scan + [t_time] * self._n_frames_actual_bliss_scan ) _logger.warning(message) else: @@ -575,7 +575,7 @@ class StandardAcquisition(BaseAcquisition): frames_times_stamps_as_f8 = numpy.linspace( start=str_datetime_to_numpy_datetime64(start_time).astype("f8"), stop=str_datetime_to_numpy_datetime64(end_time).astype("f8"), - num=self._n_frames_current_bliss_scan, + num=self._n_frames_actual_bliss_scan, endpoint=True, dtype=" Date: Mon, 9 May 2022 09:16:57 +0200 Subject: [PATCH 12/23] edf2nx: define electric current: fix some typos --- nxtomomill/converter/edf/edfconverter.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/nxtomomill/converter/edf/edfconverter.py b/nxtomomill/converter/edf/edfconverter.py index b0b9815..416aefe 100644 --- a/nxtomomill/converter/edf/edfconverter.py +++ b/nxtomomill/converter/edf/edfconverter.py @@ -740,7 +740,7 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: # try to create the current dataset electric_current = scan.retrieve_information( scan=scan.path, - dataset_basename=scan.path, + dataset_basename=scan.dataset_basename, ref_file=None, key="SrCurrent", key_aliases=["SRCUR", "machineCurrentStart"], @@ -749,12 +749,9 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: ) nexus_paths = get_nexus_paths(LATEST_VERSION) if electric_current is not None: - electric_current = h5d["/entry"].create_dataset( - nexus_paths.ELECTRIC_CURRENT_PATH, - shape=(n_frames,), - dtype=numpy.float32, + h5d["/entry"][nexus_paths.ELECTRIC_CURRENT_PATH] = numpy.asarray( + [electric_current] * n_frames ) - electric_current[:] = electric_current h5d.flush() -- GitLab From c426e8df4628ccf3fd85df99fc43b23795d2977e Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Mon, 9 May 2022 14:07:05 +0200 Subject: [PATCH 13/23] edf2nx: improve handling of current. Now read them from the header instead of the .info file --- nxtomomill/converter/edf/edfconverter.py | 83 ++++++++++++++++++------ nxtomomill/io/config/configbase.py | 9 +++ nxtomomill/io/config/edfconfig.py | 1 + nxtomomill/io/config/hdf5config.py | 8 --- nxtomomill/settings.py | 2 + 5 files changed, 76 insertions(+), 27 deletions(-) diff --git a/nxtomomill/converter/edf/edfconverter.py b/nxtomomill/converter/edf/edfconverter.py index 416aefe..166f759 100644 --- a/nxtomomill/converter/edf/edfconverter.py +++ b/nxtomomill/converter/edf/edfconverter.py @@ -70,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__) @@ -86,6 +87,7 @@ EDFFileKeys = namedtuple( "to_ignore", "dark_names", "ref_names", + "machine_elec_current_keys", ], ) @@ -99,6 +101,7 @@ DEFAULT_EDF_KEYS = EDFFileKeys( EDF_TO_IGNORE, EDF_DARK_NAMES, EDF_REFS_NAMES, + EDF_MACHINE_ELECTRIC_CURRENT, ) @@ -152,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 @@ -198,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 @@ -250,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: @@ -259,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 @@ -275,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, @@ -283,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: @@ -391,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 @@ -515,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) @@ -583,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 @@ -673,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 @@ -737,22 +798,6 @@ def from_edf_to_nx(configuration: TomoEDFConfig, progress=None) -> tuple: if source_grp is not None and "NX_class" not in source_grp.attrs: source_grp.attrs["NX_class"] = "NXsource" - # try to create the current dataset - electric_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, - ) - nexus_paths = get_nexus_paths(LATEST_VERSION) - if electric_current is not None: - h5d["/entry"][nexus_paths.ELECTRIC_CURRENT_PATH] = numpy.asarray( - [electric_current] * n_frames - ) - h5d.flush() for i_meta, meta in enumerate(metadata): diff --git a/nxtomomill/io/config/configbase.py b/nxtomomill/io/config/configbase.py index f95a5b4..7db8356 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 d8b8959..b32a053 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 969cfac..da9db61 100644 --- a/nxtomomill/io/config/hdf5config.py +++ b/nxtomomill/io/config/hdf5config.py @@ -677,14 +677,6 @@ class TomoHDF5Config(ConfigBase): else: self._sample_detector_distance_keys = paths - @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 - # frame type definition def _parse_frame_urls(self, urls: tuple): diff --git a/nxtomomill/settings.py b/nxtomomill/settings.py index 4b0bd9a..94be932 100644 --- a/nxtomomill/settings.py +++ b/nxtomomill/settings.py @@ -143,6 +143,8 @@ class Tomo: Z_TRANS = ("sz",) + MACHINE_ELECTRIC_CURRENT = ("srcur", "srcurrent") + # EDF_TO_IGNORE = ['HST', '_slice_'] TO_IGNORE = ("_slice_",) -- GitLab From 6189321240a351167d6a5a17c412bb2d3d243086 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Tue, 10 May 2022 08:21:12 +0200 Subject: [PATCH 14/23] hdf5: ger_electric_current: update indentation to increase readability --- nxtomomill/converter/hdf5/acquisition/baseacquisition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxtomomill/converter/hdf5/acquisition/baseacquisition.py b/nxtomomill/converter/hdf5/acquisition/baseacquisition.py index 6a5773c..f2d531d 100644 --- a/nxtomomill/converter/hdf5/acquisition/baseacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/baseacquisition.py @@ -505,7 +505,7 @@ class BaseAcquisition: f"Unable to retrieve machine electric current for {root_node.name}" ) - return None, None + return None, None def _get_rotation_angle(self, root_node, n_frame) -> tuple: """return the list of rotation angle for each frame""" -- GitLab From a58c00ea5e8313ffee7af4705aa6ea557cdac778 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Tue, 10 May 2022 08:40:42 +0200 Subject: [PATCH 15/23] hdf5 converter: add doc for the linspace within datetime --- .../converter/hdf5/acquisition/standardacquisition.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index 6c50323..d6ef955 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -541,9 +541,12 @@ class StandardAcquisition(BaseAcquisition): str_datetime_to_numpy_datetime64(end_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("f8"), - stop=str_datetime_to_numpy_datetime64(end_time).astype("f8"), + start=str_datetime_to_numpy_datetime64(start_time).astype("f16"), + stop=str_datetime_to_numpy_datetime64(end_time).astype("f16"), num=len(electric_currents), endpoint=True, dtype=" Date: Tue, 10 May 2022 08:55:48 +0200 Subject: [PATCH 16/23] nxmonitor: add concatenate function --- nxtomomill/nexus/nxmonitor.py | 18 ++++++++++++++++++ nxtomomill/nexus/nxtomo.py | 5 +++++ nxtomomill/nexus/test/test_nxmonitor.py | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/nxtomomill/nexus/nxmonitor.py b/nxtomomill/nexus/nxmonitor.py index f29d494..b17895b 100644 --- a/nxtomomill/nexus/nxmonitor.py +++ b/nxtomomill/nexus/nxmonitor.py @@ -29,6 +29,8 @@ __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 @@ -103,3 +105,19 @@ class NXmonitor(NXobject): 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 90200f3..635fe6e 100644 --- a/nxtomomill/nexus/nxtomo.py +++ b/nxtomomill/nexus/nxtomo.py @@ -407,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 index 4591d80..fe0f748 100644 --- a/nxtomomill/nexus/test/test_nxmonitor.py +++ b/nxtomomill/nexus/test/test_nxmonitor.py @@ -30,6 +30,7 @@ __date__ = "02/05/2022" from nxtomomill.nexus.nxmonitor import NXmonitor +from nxtomomill.nexus import concatenate import pytest import numpy @@ -46,3 +47,22 @@ def test_nx_sample(): 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, + ] + ), + ) -- GitLab From 92dc5d8a1418b3e2e9a2e19ff1bb1f7887cba8ae Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Tue, 10 May 2022 09:02:28 +0200 Subject: [PATCH 17/23] nexus: fix typo on control setter --- nxtomomill/nexus/nxtomo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxtomomill/nexus/nxtomo.py b/nxtomomill/nexus/nxtomo.py index 635fe6e..583ac45 100644 --- a/nxtomomill/nexus/nxtomo.py +++ b/nxtomomill/nexus/nxtomo.py @@ -139,7 +139,7 @@ class NXtomo(NXobject): @control.setter def control(self, control: Optional[NXmonitor]) -> None: - if not isinstance(control, (type(None), NXsample)): + if not isinstance(control, (type(None), NXmonitor)): raise TypeError( f"control is expected ot be an instance of {NXmonitor} or None. Not {type(control)}" ) -- GitLab From 33fa0a9457ce7bac37ca55bfb74b986977fe59c9 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Tue, 10 May 2022 10:02:44 +0200 Subject: [PATCH 18/23] _get_electric_current: improve doc --- nxtomomill/converter/hdf5/acquisition/baseacquisition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nxtomomill/converter/hdf5/acquisition/baseacquisition.py b/nxtomomill/converter/hdf5/acquisition/baseacquisition.py index f2d531d..f4e37b2 100644 --- a/nxtomomill/converter/hdf5/acquisition/baseacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/baseacquisition.py @@ -494,11 +494,12 @@ class BaseAcquisition: except (ValueError, KeyError): pass else: - # handle special case: elec_current is sometime a list and sometime a number... + # 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( -- GitLab From 5a2c49104e1cca9219ca859cf06e19976f385f34 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Thu, 12 May 2022 08:33:02 +0200 Subject: [PATCH 19/23] current: only register current with start_time if find only one electric current --- .../hdf5/acquisition/standardacquisition.py | 13 +++++++------ .../converter/hdf5/test/test_hdf5converter.py | 10 +++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index d6ef955..dec1fe7 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -520,15 +520,19 @@ class StandardAcquisition(BaseAcquisition): new_know_electric_currents = {} if start_time is None or end_time is None: - _logger.warning( - "Unable to find start_time and / or end_time. Will pick the first available electricl_current for the frame" - ) 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 @@ -537,9 +541,6 @@ class StandardAcquisition(BaseAcquisition): new_know_electric_currents[ str_datetime_to_numpy_datetime64(start_time) ] = (electric_currents[0] * unit_factor) - new_know_electric_currents[ - str_datetime_to_numpy_datetime64(end_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 diff --git a/nxtomomill/converter/hdf5/test/test_hdf5converter.py b/nxtomomill/converter/hdf5/test/test_hdf5converter.py index 8bfe0ce..553dbc5 100644 --- a/nxtomomill/converter/hdf5/test/test_hdf5converter.py +++ b/nxtomomill/converter/hdf5/test/test_hdf5converter.py @@ -1035,16 +1035,12 @@ def test_machine_electric_current(): "2022-01-15T21:07:58.360095+02:00", "2022-01-15T21:07:59.360095+02:00", ) - end_times = ("2022-01-15T21:07:59.170095+02:00", None) - for (node_name, machine_elec_current_ma, st, et) in zip( - node_names, machine_elec_current, start_times, end_times + 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" - if st is not None: - h5f[node_name]["start_time"] = st - if et is not None: - h5f[node_name]["end_time"] = et + h5f[node_name]["start_time"] = st assert "4.1" in h5f assert "5.1" in h5f -- GitLab From 55689b3b8bf0557ae03b356e5912c5a5cb82e221 Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Tue, 7 Jun 2022 08:55:12 +0200 Subject: [PATCH 20/23] add current: ease comprehension --- nxtomomill/converter/hdf5/acquisition/standardacquisition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index dec1fe7..8fe4c61 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -546,8 +546,8 @@ class StandardAcquisition(BaseAcquisition): # 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("f16"), - stop=str_datetime_to_numpy_datetime64(end_time).astype("f16"), + 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=" Date: Tue, 7 Jun 2022 08:58:39 +0200 Subject: [PATCH 21/23] add current: improve homogeneity --- nxtomomill/converter/hdf5/acquisition/standardacquisition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index 8fe4c61..999fb72 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -577,8 +577,8 @@ class StandardAcquisition(BaseAcquisition): _logger.error(message) else: frames_times_stamps_as_f8 = numpy.linspace( - start=str_datetime_to_numpy_datetime64(start_time).astype("f8"), - stop=str_datetime_to_numpy_datetime64(end_time).astype("f8"), + start=str_datetime_to_numpy_datetime64(start_time).astype(numpy.float128), + stop=str_datetime_to_numpy_datetime64(end_time).astype(numpy.float128), num=self._n_frames_actual_bliss_scan, endpoint=True, dtype=" Date: Tue, 7 Jun 2022 09:17:47 +0200 Subject: [PATCH 22/23] add current: fix creation of the unit factor --- nxtomomill/converter/hdf5/acquisition/standardacquisition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index 999fb72..2652031 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -514,8 +514,8 @@ class StandardAcquisition(BaseAcquisition): ) unit_factor = ( - electric_current_unit_ref.value - * ElectricCurrentSystem.from_str(electric_current_unit).value + ElectricCurrentSystem.from_str(electric_current_unit).value + / electric_current_unit_ref.value ) new_know_electric_currents = {} -- GitLab From 0239895e30e18853dc83afa8e6f56cfc2476616a Mon Sep 17 00:00:00 2001 From: Henri Payno Date: Tue, 7 Jun 2022 09:17:53 +0200 Subject: [PATCH 23/23] PEP8 --- .../hdf5/acquisition/standardacquisition.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py index 2652031..7309ed2 100644 --- a/nxtomomill/converter/hdf5/acquisition/standardacquisition.py +++ b/nxtomomill/converter/hdf5/acquisition/standardacquisition.py @@ -546,8 +546,12 @@ class StandardAcquisition(BaseAcquisition): # 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), + 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="