# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2020 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__ = "21/04/2022"
import configparser
import logging
from typing import Optional, Union
from silx.utils.enum import Enum as _Enum
from pyunitsystem.electriccurrentsystem import ElectricCurrentSystem
from pyunitsystem.energysystem import EnergySI
from pyunitsystem.metricsystem import MetricSystem
from nxtomo.nxobject.nxsource import ProbeType, SourceType
from nxtomomill.io.config.configbase import ConfigBase
from nxtomomill.io.utils import (
PathType,
convert_str_to_bool,
convert_str_to_tuple,
filter_str_def,
)
from nxtomomill.settings import Tomo
from nxtomomill.utils import FileExtension
_logger = logging.getLogger(__name__)
[docs]class OptionLevel(_Enum):
REQUIRED = "required"
ADVANCED = "advanced"
[docs]class TomoEDFConfig(ConfigBase):
"""
Configuration class to provide to the convert from h5 to nx
"""
_valid_metric_values = [
str(item)
for item in (
MetricSystem.METER,
MetricSystem.CENTIMETER,
MetricSystem.MILLIMETER,
MetricSystem.MICROMETER,
MetricSystem.NANOMETER,
)
]
_valid_energy_values = [str(item) for item in EnergySI]
_valid_elec_current_values = [str(item) for item in ElectricCurrentSystem]
# General section keys
GENERAL_SECTION_DK = "GENERAL_SECTION"
INPUT_FOLDER_DK = "input_folder"
OUTPUT_FILE_DK = "output_file"
FILE_EXTENSION_DK = "file_extension"
OVERWRITE_DK = "overwrite"
DELETE_EDF_SOURCE_FILES = "delete_edf_source_file"
OUTPUT_CHECKS = "output_checks"
DATASET_BASENAME_DK = "dataset_basename"
DATASET_FILE_INFO_DK = "dataset_info_file"
LOG_LEVEL_DK = "log_level"
TITLE_DK = "title"
IGNORE_FILE_PATTERN_DK = "patterns_to_ignores"
DUPLICATE_DATA_DK = "duplicate_data"
EXTERNAL_LINK_RELATIVE_DK = "external_link_path"
COMMENTS_GENERAL_SECTION = {
GENERAL_SECTION_DK: "general information. \n",
INPUT_FOLDER_DK: "Folder containing .edf files. if not provided from the configuration file must be provided from the command line",
OUTPUT_FILE_DK: "output file name. If not provided from the configuration file must be provided from the command line",
OVERWRITE_DK: "overwrite output files if exists without asking",
FILE_EXTENSION_DK: "file extension. Ignored if the output file is provided and contains an extension",
DELETE_EDF_SOURCE_FILES: "remove EDF source files once the conversion is complete. Works only if 'duplicate_data' is True (this is the case by default)",
OUTPUT_CHECKS: "Once the conversion is done some checks can be done to check the validity of the conversion. Expected as a list of elements. Possibles tests are: 'compare-output-volume'",
DATASET_BASENAME_DK: f"dataset file prefix. Usde to determine projections file and info file. If not provided will take the name of {INPUT_FOLDER_DK}",
DATASET_FILE_INFO_DK: f"path to .info file containing dataset information (Energy, ScanRange, TOMO_N...). If not will deduce it from {DATASET_BASENAME_DK}",
LOG_LEVEL_DK: 'Log level. Valid levels are "debug", "info", "warning" and "error"',
TITLE_DK: "NXtomo title",
IGNORE_FILE_PATTERN_DK: "some file pattern leading to ignoring the file. Like reconstructed slice files.",
DUPLICATE_DATA_DK: "If False then will create embed all the data into a single file avoiding external link to other file. If True then the decetor data will point to original edf files. In this case you must be carreful to keep relative paths valid. Warning: to read external dataset you nust be at the hdf5 file working directory. See external link resolution details: https://docs.hdfgroup.org/hdf5/v1_12/group___h5_l.html#title5",
EXTERNAL_LINK_RELATIVE_DK: "If 'duplicate_data' is set to False then you can specify if you want the link to original files to be 'relative' or 'absolute'",
}
LEVEL_GENERAL_SECTION = {
INPUT_FOLDER_DK: OptionLevel.REQUIRED,
OUTPUT_FILE_DK: OptionLevel.REQUIRED,
OVERWRITE_DK: OptionLevel.REQUIRED,
FILE_EXTENSION_DK: OptionLevel.ADVANCED,
DELETE_EDF_SOURCE_FILES: OptionLevel.ADVANCED,
OUTPUT_CHECKS: OptionLevel.ADVANCED,
DATASET_BASENAME_DK: OptionLevel.REQUIRED,
DATASET_FILE_INFO_DK: OptionLevel.REQUIRED,
LOG_LEVEL_DK: OptionLevel.REQUIRED,
TITLE_DK: OptionLevel.ADVANCED,
IGNORE_FILE_PATTERN_DK: OptionLevel.REQUIRED,
DUPLICATE_DATA_DK: OptionLevel.REQUIRED,
EXTERNAL_LINK_RELATIVE_DK: OptionLevel.ADVANCED,
}
# EDF KEYS SECTION
EDF_KEYS_SECTION_DK = "EDF_KEYS_SECTION"
MOTOR_POSITION_KEY_DK = "motor_position_key"
MOTOR_MNE_KEY_DK = "motor_mne_key"
X_TRANS_KEY_DK = "x_translation_key"
Y_TRANS_KEY_DK = "y_translation_key"
Z_TRANS_KEY_DK = "z_translation_key"
ROT_ANGLE_KEY_DK = "rot_angle_key"
COMMENTS_KEYS_SECTION = {
EDF_KEYS_SECTION_DK: "section to define EDF keys to pick from headers to deduce information like rotation angle.\n",
MOTOR_POSITION_KEY_DK: "motor position key",
MOTOR_MNE_KEY_DK: "key to retrieve indices of each motor in metadata",
X_TRANS_KEY_DK: "key to be used for x translation",
Y_TRANS_KEY_DK: "key to be used for y translation",
Z_TRANS_KEY_DK: "key to be used for z translation",
ROT_ANGLE_KEY_DK: "key to be used for rotation angle",
}
LEVEL_KEYS_SECTION = {
MOTOR_POSITION_KEY_DK: OptionLevel.REQUIRED,
MOTOR_MNE_KEY_DK: OptionLevel.REQUIRED,
X_TRANS_KEY_DK: OptionLevel.REQUIRED,
Y_TRANS_KEY_DK: OptionLevel.REQUIRED,
Z_TRANS_KEY_DK: OptionLevel.REQUIRED,
ROT_ANGLE_KEY_DK: OptionLevel.REQUIRED,
}
# DARK AND FLAT SECTION
FLAT_DARK_SECTION_DK = "DARK_AND_FLAT_SECTION"
DARK_NAMES_DK = "dark_names_prefix"
FLAT_NAMES_DK = "flat_names_prefix"
COMMENTS_DARK_FLAT_SECTION = {
FLAT_DARK_SECTION_DK: "section to define dark and flat detection. \n",
DARK_NAMES_DK: "prefix of dark field file(s)",
FLAT_NAMES_DK: "prefix of flat field file(s)",
}
LEVEL_DARK_FLAT_SECTION = {
DARK_NAMES_DK: OptionLevel.REQUIRED,
FLAT_NAMES_DK: OptionLevel.REQUIRED,
}
# UNITS SECTION
UNIT_SECTION_DK = "UNIT_SECTION"
PIXEL_SIZE_EXPECTED_UNIT = "expected_unit_for_pixel_size"
DISTANCE_EXPECTED_UNIT = "expected_unit_for_distance"
ENERGY_EXPECTED_UNIT = "expected_unit_for_energy"
X_TRANS_EXPECTED_UNIT = "expected_unit_for_x_translation"
Y_TRANS_EXPECTED_UNIT = "expected_unit_for_y_translation"
Z_TRANS_EXPECTED_UNIT = "expected_unit_for_z_translation"
MACHINE_ELEC_CURRENT_EXPECTED_UNIT = "expected_unit_for_machine_current"
COMMENTS_UNIT_SECTION_DK = {
UNIT_SECTION_DK: "Details units system used on SPEC side to save data. All will ne converted to NXtomo default (SI at the exception of energy-keV) \n",
PIXEL_SIZE_EXPECTED_UNIT: f"Size used to save pixel size. Must be in of {_valid_metric_values}",
DISTANCE_EXPECTED_UNIT: f"Unit used by SPEC to save sample to detector distance. Must be in of {_valid_metric_values}",
ENERGY_EXPECTED_UNIT: f"Unit used by SPEC to save energy. Must be in of {_valid_energy_values}",
X_TRANS_EXPECTED_UNIT: f"Unit used by SPEC to save x translation. Must be in of {_valid_metric_values}",
Y_TRANS_EXPECTED_UNIT: f"Unit used by SPEC to save y translation. Must be in of {_valid_metric_values}",
Z_TRANS_EXPECTED_UNIT: f"Unit used by SPEC to save z translation. Must be in of {_valid_metric_values}",
MACHINE_ELEC_CURRENT_EXPECTED_UNIT: f"Unit used by SPEC to save machine current (also aka SRcurrent). Must be in of {_valid_elec_current_values}",
}
LEVEL_UNIT_SECTION = {
PIXEL_SIZE_EXPECTED_UNIT: OptionLevel.ADVANCED,
DISTANCE_EXPECTED_UNIT: OptionLevel.ADVANCED,
ENERGY_EXPECTED_UNIT: OptionLevel.ADVANCED,
X_TRANS_EXPECTED_UNIT: OptionLevel.ADVANCED,
Y_TRANS_EXPECTED_UNIT: OptionLevel.ADVANCED,
Z_TRANS_EXPECTED_UNIT: OptionLevel.ADVANCED,
}
# SAMPLE SECTION
SAMPLE_SECTION_DK = "SAMPLE_SECTION"
SAMPLE_NAME_DK = "sample_name"
FORCE_ANGLE_CALCULATION = "force_angle_calculation"
FORCE_ANGLE_CALCULATION_ENDPOINT = "angle_calculation_endpoint"
FORCE_ANGLE_CALCULATION_REVERT_NEG_SCAN_RANGE = (
"angle_calculation_rev_neg_scan_range"
)
COMMENTS_SAMPLE_SECTION_DK = {
SAMPLE_SECTION_DK: "section dedicated to sample definition.\n",
SAMPLE_NAME_DK: "name of the sample",
FORCE_ANGLE_CALCULATION: "Should the rotation angle be computed from scan range and numpy.linspace or should we try to load it from .edf header.",
FORCE_ANGLE_CALCULATION_ENDPOINT: "If rotation angles have to be calculated set numpy.linspace endpoint parameter to this value. If True then the rotation angle value of the last projection will be equal to the `ScanRange` value",
FORCE_ANGLE_CALCULATION_REVERT_NEG_SCAN_RANGE: "Invert rotation angle values in the case of negative `ScanRange` value",
}
LEVEL_SAMPLE_SECTION = {
SAMPLE_NAME_DK: OptionLevel.ADVANCED,
FORCE_ANGLE_CALCULATION: OptionLevel.REQUIRED,
FORCE_ANGLE_CALCULATION_ENDPOINT: OptionLevel.REQUIRED,
FORCE_ANGLE_CALCULATION_REVERT_NEG_SCAN_RANGE: OptionLevel.ADVANCED,
}
# SOURCE SECTION
SOURCE_SECTION_DK = "SOURCE_SECTION"
INSTRUMENT_NAME_DK = "instrument_name"
SOURCE_NAME_DK = "source_name"
SOURCE_TYPE_DK = "source_type"
SOURCE_PROBE_DK = "source_probe"
COMMENTS_SOURCE_SECTION_DK = {
SOURCE_SECTION_DK: "section dedicated to source definition.\n",
INSTRUMENT_NAME_DK: "name of the instrument",
SOURCE_NAME_DK: "name of the source",
SOURCE_TYPE_DK: f"type of the source. Must be one of {SourceType.values()}",
SOURCE_PROBE_DK: f"source probe. Must be one of {ProbeType.values()}",
}
LEVEL_SOURCE_SECTION = {
INSTRUMENT_NAME_DK: OptionLevel.ADVANCED,
SOURCE_NAME_DK: OptionLevel.ADVANCED,
SOURCE_TYPE_DK: OptionLevel.ADVANCED,
SOURCE_PROBE_DK: OptionLevel.ADVANCED,
}
# DETECTOR SECTION
DETECTOR_SECTION_DK = "DETECTOR_SECTION"
FIELD_OF_VIEW_DK = "field_of_view"
COMMENTS_DETECTOR_SECTION_DK = {
DETECTOR_SECTION_DK: "section dedicated to detector definition \n",
FIELD_OF_VIEW_DK: "Detector field of view. Must be in `Half` or `Full`",
}
LEVEL_DETECTOR_SECTION = {
FIELD_OF_VIEW_DK: OptionLevel.ADVANCED,
}
# create comments
COMMENTS = COMMENTS_GENERAL_SECTION
COMMENTS.update(COMMENTS_KEYS_SECTION)
COMMENTS.update(COMMENTS_DARK_FLAT_SECTION)
COMMENTS.update(COMMENTS_UNIT_SECTION_DK)
COMMENTS.update(COMMENTS_SAMPLE_SECTION_DK)
COMMENTS.update(COMMENTS_SOURCE_SECTION_DK)
COMMENTS.update(COMMENTS_DETECTOR_SECTION_DK)
SECTIONS_LEVEL = {
GENERAL_SECTION_DK: OptionLevel.REQUIRED,
EDF_KEYS_SECTION_DK: OptionLevel.REQUIRED,
FLAT_DARK_SECTION_DK: OptionLevel.REQUIRED,
UNIT_SECTION_DK: OptionLevel.ADVANCED,
SAMPLE_SECTION_DK: OptionLevel.REQUIRED,
SOURCE_SECTION_DK: OptionLevel.ADVANCED,
DETECTOR_SECTION_DK: OptionLevel.ADVANCED,
}
[docs] def __init__(self):
super().__init__()
self._set_freeze(False)
# general information
self._input_folder = None
self._output_file = None
self._file_extension = FileExtension.NX
self._overwrite = False
self._delete_edf_source_files = False
self._output_checks = tuple()
self._dataset_basename = None
self._log_level = logging.WARNING
self._title = None
self._ignore_file_patterns = Tomo.EDF.TO_IGNORE
self._dataset_info_file = None
self._duplicate_data = True
self._external_path_type = PathType.RELATIVE
# edf header keys
self._motor_position_keys = Tomo.EDF.MOTOR_POS
self._motor_mne_keys = Tomo.EDF.MOTOR_MNE
self._x_trans_keys = Tomo.EDF.X_TRANS
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
self._flat_names = Tomo.EDF.REFS_NAMES
# units
self._pixel_size_unit = MetricSystem.MICROMETER
self._distance_unit = MetricSystem.MILLIMETER
self._energy_unit = EnergySI.KILOELECTRONVOLT
self._x_trans_unit = MetricSystem.MILLIMETER
self._y_trans_unit = MetricSystem.MILLIMETER
self._z_trans_unit = MetricSystem.MILLIMETER
self._machine_elec_current_unit = ElectricCurrentSystem.MILLIAMPERE
# sample
self._sample_name = None
self._force_angle_calculation = True
# there is too many EDF headers containing the rotation angle set to 0
# when it shouldn't be. To ease usage we are 'ignoring' those keys by default...
self._force_angle_calculation_endpoint = False
self._force_angle_calculation_revert_neg_scan_range = True
# source
self._instrument_name = None
self._source_name = "ESRF"
self._source_type = SourceType.SYNCHROTRON_X_RAY_SOURCE
self._source_probe = ProbeType.X_RAY
# detector
self._field_of_view = None
self._set_freeze(True)
@property
def input_folder(self) -> Optional[str]:
return self._input_folder
@input_folder.setter
def input_folder(self, folder: Optional[str]) -> None:
if not isinstance(folder, (type(None), str)):
raise TypeError(
f"folder is expected to be None or an instance of str. Not {type(folder)}"
)
self._input_folder = folder
@property
def dataset_basename(self) -> Optional[str]:
return self._dataset_basename
@dataset_basename.setter
def dataset_basename(self, dataset_basename: Optional[str]) -> None:
if not isinstance(dataset_basename, (type(None), str)):
raise TypeError(
f"dataset_basename is expected to be None or an instance of str. Not {type(dataset_basename)}"
)
self._dataset_basename = dataset_basename
@property
def dataset_info_file(self) -> Optional[str]:
return self._dataset_info_file
@dataset_info_file.setter
def dataset_info_file(self, file_path: Optional[str]) -> None:
if not isinstance(file_path, (type(None), str)):
raise TypeError(
f"file_path is expected to be None or an instance of str. Not {type(file_path)}"
)
self._dataset_info_file = file_path
@property
def duplicate_data(self) -> bool:
return self._duplicate_data
@duplicate_data.setter
def duplicate_data(self, duplicate: bool):
if not isinstance(duplicate, bool):
raise TypeError(
f"duplicate is expected to be a bool and not {type(duplicate)}"
)
self._duplicate_data = duplicate
@property
def external_path_type(self) -> PathType:
return self._external_path_type
@external_path_type.setter
def external_path_type(self, path_type: Union[str, PathType]):
self._external_path_type = PathType.from_value(path_type)
@property
def title(self) -> Optional[str]:
return self._title
@title.setter
def title(self, title: Optional[str]) -> None:
if not isinstance(title, (type(None), str)):
raise TypeError(
f"title is expected to be None or an instance of str. Not {type(title)}"
)
self._title = title
@property
def delete_edf_source_files(self) -> bool:
return self._delete_edf_source_files
@delete_edf_source_files.setter
def delete_edf_source_files(self, delete: bool) -> None:
if not isinstance(delete, bool):
raise TypeError("'delete' is expected to be a bool")
self._delete_edf_source_files = delete
@property
def output_checks(self) -> tuple:
return self._output_checks
@output_checks.setter
def output_checks(self, checks: Optional[tuple]) -> None:
if checks is None:
self._output_checks = tuple()
elif not isinstance(checks, tuple):
raise TypeError("'checks' is expected to be None or a tuple")
else:
self._output_checks = checks
@property
def ignore_file_patterns(self) -> tuple:
return self._ignore_file_patterns
@ignore_file_patterns.setter
def ignore_file_patterns(self, patterns: Optional[Union[tuple, list]]):
if not isinstance(patterns, (type(None), tuple, list)):
raise TypeError("patterns is expected to be a tuple or a list")
if patterns is None:
self._ignore_file_patterns = tuple()
else:
for elmt in patterns:
if not isinstance(elmt, str):
raise TypeError("patterns elmts are expected to be str")
self._ignore_file_patterns = tuple(patterns)
@property
def motor_position_keys(self) -> tuple:
return self._motor_position_keys
@motor_position_keys.setter
def motor_position_keys(self, keys: Optional[Union[tuple, list]]) -> None:
if not isinstance(keys, (type(None), tuple, list)):
raise TypeError(
"keys is expected to be None or an instance of list or tuple"
)
if keys is None:
self._motor_position_keys = tuple()
else:
for elmt in keys:
if not isinstance(elmt, str):
raise TypeError("keys elmts are expected to be str")
self._motor_position_keys = tuple(keys)
@property
def motor_mne_keys(self) -> tuple:
return self._motor_mne_keys
@motor_mne_keys.setter
def motor_mne_keys(self, keys: Optional[Union[tuple, list]]) -> None:
if not isinstance(keys, (type(None), tuple, list)):
raise TypeError(
"keys is expected to be None or an instance of list or tuple"
)
if keys is None:
self._motor_mne_keys = tuple()
else:
for elmt in keys:
if not isinstance(elmt, str):
raise TypeError("keys elmts are expected to be str")
self._motor_mne_keys = tuple(keys)
@property
def dark_names(self) -> tuple:
return self._dark_names
@dark_names.setter
def dark_names(self, names: Optional[Union[tuple, list]]) -> None:
if not isinstance(names, (type(None), tuple, list)):
raise TypeError("names is expected to be a tuple or a list")
if names is None:
self._dark_names = tuple()
else:
for elmt in names:
if not isinstance(elmt, str):
raise TypeError("names elmts are expected to be str")
self._dark_names = tuple(names)
@property
def flat_names(self) -> tuple:
return self._flat_names
@flat_names.setter
def flat_names(self, names: Optional[Union[tuple, list]]) -> None:
if not isinstance(names, (type(None), tuple, list)):
raise TypeError("names is expected to be a tuple or a list")
if names is None:
self._flat_names = tuple()
else:
for elmt in names:
if not isinstance(elmt, str):
raise TypeError("names elmts are expected to be str")
self._flat_names = tuple(names)
@property
def pixel_size_unit(self) -> MetricSystem:
return self._pixel_size_unit
@pixel_size_unit.setter
def pixel_size_unit(self, unit: Union[MetricSystem, str]) -> None:
if not isinstance(unit, (MetricSystem, str)):
raise TypeError("unit is expected to be an instance of MetricSystem")
if isinstance(unit, str):
unit = MetricSystem.from_str(unit)
self._pixel_size_unit = unit
@property
def distance_unit(self) -> MetricSystem:
return self._distance_unit
@distance_unit.setter
def distance_unit(self, unit: Union[MetricSystem, str]) -> None:
if not isinstance(unit, (MetricSystem, str)):
raise TypeError("unit is expected to be an instance of MetricSystem")
if isinstance(unit, str):
unit = MetricSystem.from_str(unit)
self._distance_unit = unit
@property
def energy_unit(self) -> EnergySI:
return self._energy_unit
@energy_unit.setter
def energy_unit(self, unit: Union[EnergySI, str]) -> None:
if not isinstance(unit, (EnergySI, str)):
raise TypeError("unit is expected to be an instance of EnergySI")
if isinstance(unit, str):
unit = EnergySI.from_str(unit)
self._energy_unit = unit
@property
def x_trans_unit(self) -> MetricSystem:
return self._x_trans_unit
@x_trans_unit.setter
def x_trans_unit(self, unit: Union[MetricSystem, str]) -> None:
if not isinstance(unit, (MetricSystem, str)):
raise TypeError("unit is expected to be an instance of MetricSystem")
if isinstance(unit, str):
unit = MetricSystem.from_str(unit)
self._x_trans_unit = unit
@property
def y_trans_unit(self) -> MetricSystem:
return self._y_trans_unit
@y_trans_unit.setter
def y_trans_unit(self, unit: Union[MetricSystem, str]) -> None:
if not isinstance(unit, (MetricSystem, str)):
raise TypeError("unit is expected to be an instance of MetricSystem")
if isinstance(unit, str):
unit = MetricSystem.from_str(unit)
self._y_trans_unit = unit
@property
def z_trans_unit(self) -> MetricSystem:
return self._z_trans_unit
@z_trans_unit.setter
def z_trans_unit(self, unit: Union[MetricSystem, str]) -> None:
if not isinstance(unit, (MetricSystem, str)):
raise TypeError("unit is expected to be an instance of MetricSystem")
if isinstance(unit, str):
unit = MetricSystem.from_str(unit)
self._z_trans_unit = unit
@property
def machine_elec_current_unit(self) -> ElectricCurrentSystem:
return self._machine_elec_current_unit
@machine_elec_current_unit.setter
def machine_elec_current_unit(self, unit):
if not isinstance(unit, (str, ElectricCurrentSystem)):
raise TypeError(
"unit is expected to be an instance of ElectricCurrentSystem"
)
if isinstance(unit, str):
unit = ElectricCurrentSystem.from_str(unit)
self._machine_elec_current_unit = unit
@property
def sample_name(self) -> Optional[str]:
return self._sample_name
@sample_name.setter
def sample_name(self, name: Optional[str]) -> None:
if not isinstance(name, (type(None), str)):
raise TypeError("name is expected to be None or an instance of str")
self._sample_name = name
@property
def force_angle_calculation(self) -> bool:
return self._force_angle_calculation
@force_angle_calculation.setter
def force_angle_calculation(self, force: bool):
if not isinstance(force, bool):
raise TypeError(
f"force is expected to be an instance of bool. Not {type(force)} - {force}"
)
else:
self._force_angle_calculation = force
@property
def force_angle_calculation_endpoint(self) -> bool:
return self._force_angle_calculation_endpoint
@force_angle_calculation_endpoint.setter
def force_angle_calculation_endpoint(self, endpoint: bool) -> None:
if not isinstance(endpoint, bool):
raise TypeError(
f"endpoint is expected to be an instance of bool. Not {type(endpoint)}"
)
else:
self._force_angle_calculation_endpoint = endpoint
@property
def angle_calculation_rev_neg_scan_range(self) -> bool:
return self._force_angle_calculation_revert_neg_scan_range
@angle_calculation_rev_neg_scan_range.setter
def angle_calculation_rev_neg_scan_range(self, revert: bool):
if not isinstance(revert, bool):
raise TypeError(
f"revert is expected to be an instance of bool. Not {type(revert)}"
)
else:
self._force_angle_calculation_revert_neg_scan_range = revert
@property
def instrument_name(self) -> Optional[str]:
return self._instrument_name
@instrument_name.setter
def instrument_name(self, name: Optional[str]):
if not isinstance(name, (type(None), str)):
raise TypeError("name is expected to be None or an instance of str")
self._instrument_name = name
@property
def source_name(self) -> Optional[str]:
return self._source_name
@source_name.setter
def source_name(self, name: Optional[str]) -> None:
if not isinstance(name, (type(None), str)):
raise TypeError("name is expected to be None or an instance of str")
self._source_name = name
@property
def source_type(self) -> Optional[SourceType]:
return self._source_type
@source_type.setter
def source_type(self, source_type: Optional[Union[SourceType, str]]):
if not isinstance(source_type, (type(None), str, SourceType)):
raise TypeError(
"source_type is expected to be None or an instance of SourceType or str"
)
if source_type is None:
self._source_type = None
else:
self._source_type = SourceType.from_value(source_type)
@property
def source_probe(self) -> Optional[ProbeType]:
return self._source_probe
@source_probe.setter
def source_probe(self, source_probe: Optional[Union[ProbeType, str]]):
if not isinstance(source_probe, (type(None), str, ProbeType)):
raise TypeError(
"source_probe is expected to be None or an instance of ProbeType or str"
)
if source_probe is None:
self._source_probe = None
else:
self._source_probe = ProbeType.from_value(source_probe)
[docs] def to_dict(self, level="advanced") -> dict:
"""convert the configuration to a dictionary"""
level = OptionLevel.from_value(level)
sections_callback = {
self.GENERAL_SECTION_DK: self._general_section_to_dict,
self.EDF_KEYS_SECTION_DK: self._edf_keys_section_to_dict,
self.FLAT_DARK_SECTION_DK: self._flat_keys_section_to_dict,
self.UNIT_SECTION_DK: self._unit_section_to_dict,
self.SAMPLE_SECTION_DK: self._sample_section_to_dict,
self.SOURCE_SECTION_DK: self._source_section_to_dict,
self.DETECTOR_SECTION_DK: self._detector_section_to_dict,
}
res = {}
for section, callback in sections_callback.items():
if (
level == OptionLevel.ADVANCED
or TomoEDFConfig.SECTIONS_LEVEL[section] == OptionLevel.REQUIRED
):
res[section] = callback(level=level)
return res
@staticmethod
def _filter_dict_keys(dict_, level, level_ref) -> dict:
keys = tuple(dict_.keys())
for key in keys:
if level == OptionLevel.REQUIRED and level_ref[key] == OptionLevel.ADVANCED:
del dict_[key]
return dict_
def _general_section_to_dict(self, level) -> dict:
res = {
self.INPUT_FOLDER_DK: (
self.input_folder if self.input_folder is not None else ""
),
self.OUTPUT_FILE_DK: (
self.output_file if self.output_file is not None else ""
),
self.OVERWRITE_DK: self.overwrite,
self.DELETE_EDF_SOURCE_FILES: self.delete_edf_source_files,
self.OUTPUT_CHECKS: self.output_checks,
self.FILE_EXTENSION_DK: self.file_extension.value,
self.DATASET_BASENAME_DK: (
self.dataset_basename if self.dataset_basename is not None else ""
),
self.DATASET_FILE_INFO_DK: (
self.dataset_info_file if self.dataset_info_file is not None else ""
),
self.LOG_LEVEL_DK: logging.getLevelName(self.log_level).lower(),
self.TITLE_DK: self.title if self.title is not None else "",
self.IGNORE_FILE_PATTERN_DK: (
self.ignore_file_patterns
if self.ignore_file_patterns != tuple()
else ""
),
self.DUPLICATE_DATA_DK: self.duplicate_data,
self.EXTERNAL_LINK_RELATIVE_DK: self.external_path_type.value,
}
return self._filter_dict_keys(
dict_=res, level=level, level_ref=TomoEDFConfig.LEVEL_GENERAL_SECTION
)
def _edf_keys_section_to_dict(self, level) -> dict:
res = {
self.MOTOR_POSITION_KEY_DK: (
self.motor_position_keys if self.motor_position_keys != tuple() else ""
),
self.MOTOR_MNE_KEY_DK: (
self.motor_mne_keys if self.motor_mne_keys != tuple() else ""
),
self.ROT_ANGLE_KEY_DK: (
self.rotation_angle_keys if self.rotation_angle_keys != tuple() else ""
),
self.X_TRANS_KEY_DK: (
self.x_trans_keys if self.x_trans_keys != tuple() else ""
),
self.Y_TRANS_KEY_DK: (
self.y_trans_keys if self.y_trans_keys != tuple() else ""
),
self.Z_TRANS_KEY_DK: (
self.z_trans_keys if self.z_trans_keys != tuple() else ""
),
}
return self._filter_dict_keys(
dict_=res, level=level, level_ref=TomoEDFConfig.LEVEL_KEYS_SECTION
)
def _flat_keys_section_to_dict(self, level) -> dict:
res = {
self.DARK_NAMES_DK: self.dark_names if self.dark_names != tuple() else "",
self.FLAT_NAMES_DK: self.flat_names if self.dark_names != tuple() else "",
}
return self._filter_dict_keys(
dict_=res, level=level, level_ref=TomoEDFConfig.LEVEL_DARK_FLAT_SECTION
)
def _unit_section_to_dict(self, level) -> dict:
res = {
self.PIXEL_SIZE_EXPECTED_UNIT: str(self.pixel_size_unit),
self.DISTANCE_EXPECTED_UNIT: str(self.distance_unit),
self.ENERGY_EXPECTED_UNIT: str(self.energy_unit),
self.X_TRANS_EXPECTED_UNIT: str(self.x_trans_unit),
self.Y_TRANS_EXPECTED_UNIT: str(self.y_trans_unit),
self.Z_TRANS_EXPECTED_UNIT: str(self.z_trans_unit),
self.MACHINE_ELEC_CURRENT_EXPECTED_UNIT: str(
self.machine_elec_current_unit
),
}
return self._filter_dict_keys(
dict_=res, level=level, level_ref=TomoEDFConfig.LEVEL_UNIT_SECTION
)
def _sample_section_to_dict(self, level) -> dict:
res = {
self.SAMPLE_NAME_DK: (
self.sample_name if self.sample_name is not None else ""
),
self.FORCE_ANGLE_CALCULATION: self.force_angle_calculation,
self.FORCE_ANGLE_CALCULATION_ENDPOINT: self.force_angle_calculation_endpoint,
self.FORCE_ANGLE_CALCULATION_REVERT_NEG_SCAN_RANGE: self.angle_calculation_rev_neg_scan_range,
}
return self._filter_dict_keys(
dict_=res, level=level, level_ref=TomoEDFConfig.LEVEL_SAMPLE_SECTION
)
def _source_section_to_dict(self, level) -> dict:
res = {
self.INSTRUMENT_NAME_DK: self.instrument_name or "",
self.SOURCE_NAME_DK: self.source_name or "",
self.SOURCE_TYPE_DK: (
self.source_type.value if self.source_type is not None else ""
),
self.SOURCE_PROBE_DK: (
self.source_probe.value if self.source_probe is not None else ""
),
}
return self._filter_dict_keys(
dict_=res, level=level, level_ref=TomoEDFConfig.LEVEL_SOURCE_SECTION
)
def _detector_section_to_dict(self, level) -> dict:
res = {
self.FIELD_OF_VIEW_DK: (
self.field_of_view.value if self.field_of_view is not None else ""
),
}
return self._filter_dict_keys(
dict_=res, level=level, level_ref=TomoEDFConfig.LEVEL_DETECTOR_SECTION
)
[docs] @staticmethod
def from_dict(dict_: dict):
r"""
Create a HDF5Config object and set it from values contained in the
dictionary
:param dict dict\_: settings dictionary
:return: HDF5Config
"""
config = TomoEDFConfig()
config.load_from_dict(dict_)
return config
[docs] def load_from_dict(self, dict_: dict) -> None:
"""Load the configuration from a dictionary"""
sections_loaders = {
TomoEDFConfig.GENERAL_SECTION_DK: self.load_general_section,
TomoEDFConfig.EDF_KEYS_SECTION_DK: self.load_keys_section,
TomoEDFConfig.FLAT_DARK_SECTION_DK: self.load_flat_dark_section,
TomoEDFConfig.UNIT_SECTION_DK: self.load_unit_section,
TomoEDFConfig.SAMPLE_SECTION_DK: self.load_sample_section,
TomoEDFConfig.SOURCE_SECTION_DK: self.load_source_section,
TomoEDFConfig.DETECTOR_SECTION_DK: self.load_detector_section,
}
for section_key, loaded_func in sections_loaders.items():
if section_key in dict_:
loaded_func(dict_[section_key])
else:
_logger.info(
f"No {section_key} section found. Will take default values"
)
def load_general_section(self, dict_: dict) -> None:
self.input_folder = dict_.get(TomoEDFConfig.INPUT_FOLDER_DK, None)
self.output_file = dict_.get(TomoEDFConfig.OUTPUT_FILE_DK, None)
overwrite = dict_.get(TomoEDFConfig.OVERWRITE_DK, None)
if overwrite is not None:
self.overwrite = convert_str_to_bool(overwrite)
delete_edf_source_files = dict_.get(TomoEDFConfig.DELETE_EDF_SOURCE_FILES, None)
if delete_edf_source_files is not None:
self.delete_edf_source_files = convert_str_to_bool(delete_edf_source_files)
output_checks = dict_.get(TomoEDFConfig.OUTPUT_CHECKS, None)
if output_checks is not None:
self.output_checks = convert_str_to_tuple(output_checks)
file_extension = dict_.get(TomoEDFConfig.FILE_EXTENSION_DK, None)
if file_extension not in (None, ""):
self.file_extension = filter_str_def(file_extension)
dataset_basename = dict_.get(TomoEDFConfig.DATASET_BASENAME_DK, None)
if dataset_basename is not None:
if dataset_basename == "":
dataset_basename = None
self.dataset_basename = dataset_basename
dataset_info_file = dict_.get(TomoEDFConfig.DATASET_FILE_INFO_DK, None)
if dataset_info_file is not None:
if dataset_info_file == "":
dataset_info_file = None
self.dataset_info_file = dataset_info_file
log_level = dict_.get(TomoEDFConfig.LOG_LEVEL_DK, None)
if log_level is not None:
self.log_level = log_level
self.title = dict_.get(TomoEDFConfig.TITLE_DK)
ignore_file_patterns = dict_.get(TomoEDFConfig.IGNORE_FILE_PATTERN_DK, None)
if ignore_file_patterns is not None:
if ignore_file_patterns == "":
ignore_file_patterns = tuple()
else:
ignore_file_patterns = convert_str_to_tuple(ignore_file_patterns)
self.ignore_file_patterns = ignore_file_patterns
duplicate_data = dict_.get(TomoEDFConfig.DUPLICATE_DATA_DK, None)
if duplicate_data is not None:
self.duplicate_data = convert_str_to_bool(duplicate_data)
external_path_type = dict_.get(TomoEDFConfig.EXTERNAL_LINK_RELATIVE_DK, None)
if external_path_type is not None:
self.external_path_type = external_path_type
def load_keys_section(self, dict_: dict) -> None:
motor_position_keys = dict_.get(TomoEDFConfig.MOTOR_POSITION_KEY_DK, None)
if motor_position_keys is not None:
if motor_position_keys == "":
motor_position_keys = tuple()
else:
motor_position_keys = convert_str_to_tuple(motor_position_keys)
self.motor_position_keys = motor_position_keys
motor_mne_keys = dict_.get(TomoEDFConfig.MOTOR_MNE_KEY_DK, None)
if motor_mne_keys is not None:
if motor_mne_keys == "":
motor_mne_keys = tuple()
else:
motor_mne_keys = convert_str_to_tuple(motor_mne_keys)
self.motor_mne_keys = motor_mne_keys
rotation_angle_keys = dict_.get(TomoEDFConfig.ROT_ANGLE_KEY_DK, None)
if rotation_angle_keys is not None:
if rotation_angle_keys == "":
rotation_angle_keys = tuple()
else:
rotation_angle_keys = convert_str_to_tuple(rotation_angle_keys)
self.rotation_angle_keys = rotation_angle_keys
x_trans_keys = dict_.get(TomoEDFConfig.X_TRANS_KEY_DK, None)
if x_trans_keys is not None:
if x_trans_keys == "":
x_trans_keys = tuple()
else:
x_trans_keys = convert_str_to_tuple(x_trans_keys)
self.x_trans_keys = x_trans_keys
y_trans_keys = dict_.get(TomoEDFConfig.Y_TRANS_KEY_DK, None)
if y_trans_keys is not None:
if y_trans_keys == "":
y_trans_keys = tuple()
else:
y_trans_keys = convert_str_to_tuple(y_trans_keys)
self.y_trans_keys = y_trans_keys
z_trans_keys = dict_.get(TomoEDFConfig.Z_TRANS_KEY_DK, None)
if z_trans_keys is not None:
if z_trans_keys == "":
z_trans_keys = tuple()
else:
z_trans_keys = convert_str_to_tuple(z_trans_keys)
self.z_trans_keys = z_trans_keys
def load_flat_dark_section(self, dict_: dict) -> None:
dark_names = dict_.get(TomoEDFConfig.DARK_NAMES_DK, None)
if dark_names is not None:
if dark_names == "":
dark_names = tuple()
else:
dark_names = convert_str_to_tuple(dark_names)
self.dark_names = dark_names
flat_names = dict_.get(TomoEDFConfig.FLAT_NAMES_DK, None)
if flat_names is not None:
if flat_names == "":
flat_names = tuple()
else:
flat_names = convert_str_to_tuple(flat_names)
self.flat_names = flat_names
def load_unit_section(self, dict_: dict) -> None:
if TomoEDFConfig.PIXEL_SIZE_EXPECTED_UNIT in dict_:
self.pixel_size_unit = MetricSystem.from_str(
dict_.get(TomoEDFConfig.PIXEL_SIZE_EXPECTED_UNIT)
)
if TomoEDFConfig.DISTANCE_EXPECTED_UNIT in dict_:
self.distance_unit = MetricSystem.from_str(
dict_.get(TomoEDFConfig.DISTANCE_EXPECTED_UNIT)
)
if TomoEDFConfig.ENERGY_EXPECTED_UNIT in dict_:
self.energy_unit = EnergySI.from_str(
dict_.get(TomoEDFConfig.ENERGY_EXPECTED_UNIT)
)
if TomoEDFConfig.X_TRANS_EXPECTED_UNIT in dict_:
self.x_trans_unit = MetricSystem.from_str(
dict_.get(TomoEDFConfig.X_TRANS_EXPECTED_UNIT)
)
if TomoEDFConfig.Y_TRANS_EXPECTED_UNIT in dict_:
self.y_trans_unit = MetricSystem.from_str(
dict_.get(TomoEDFConfig.Y_TRANS_EXPECTED_UNIT)
)
if TomoEDFConfig.Z_TRANS_EXPECTED_UNIT in dict_:
self.z_trans_unit = MetricSystem.from_str(
dict_.get(TomoEDFConfig.Z_TRANS_EXPECTED_UNIT)
)
if TomoEDFConfig.MACHINE_ELEC_CURRENT_EXPECTED_UNIT in dict_:
self.machine_elec_current_unit = ElectricCurrentSystem.from_str(
dict_.get(TomoEDFConfig.MACHINE_ELEC_CURRENT_EXPECTED_UNIT)
)
def load_sample_section(self, dict_: dict) -> None:
if TomoEDFConfig.SAMPLE_NAME_DK in dict_:
self.sample_name = dict_.get(TomoEDFConfig.SAMPLE_NAME_DK)
force_angle_calculation = dict_.get(TomoEDFConfig.FORCE_ANGLE_CALCULATION, None)
if force_angle_calculation is not None:
self.force_angle_calculation = convert_str_to_bool(force_angle_calculation)
force_angle_calculation_endpoint = dict_.get(
TomoEDFConfig.FORCE_ANGLE_CALCULATION_ENDPOINT, None
)
if force_angle_calculation_endpoint is not None:
self.force_angle_calculation_endpoint = convert_str_to_bool(
force_angle_calculation_endpoint
)
angle_calculation_rev_neg_scan_range = dict_.get(
TomoEDFConfig.FORCE_ANGLE_CALCULATION_REVERT_NEG_SCAN_RANGE, None
)
if angle_calculation_rev_neg_scan_range is not None:
self.angle_calculation_rev_neg_scan_range = convert_str_to_bool(
angle_calculation_rev_neg_scan_range
)
def load_source_section(self, dict_: dict) -> None:
if TomoEDFConfig.INSTRUMENT_NAME_DK in dict_:
self.instrument_name = dict_.get(TomoEDFConfig.INSTRUMENT_NAME_DK)
if TomoEDFConfig.SOURCE_NAME_DK in dict_:
self.source_name = dict_.get(TomoEDFConfig.SOURCE_NAME_DK)
if TomoEDFConfig.SOURCE_TYPE_DK in dict_:
self.source_type = dict_[TomoEDFConfig.SOURCE_TYPE_DK]
if TomoEDFConfig.SOURCE_PROBE_DK in dict_:
self.source_probe = dict_[TomoEDFConfig.SOURCE_PROBE_DK]
def load_detector_section(self, dict_: dict) -> None:
field_of_view = dict_.get(TomoEDFConfig.FIELD_OF_VIEW_DK, None)
if field_of_view is not None:
if field_of_view == "":
field_of_view = None
self.field_of_view = field_of_view
def to_cfg_file(self, file_path: str):
# TODO: add some generic information like:provided order of the tuple
# will be the effective one. You can provide a key from it names if
# it is contained in the positioners group
# maybe split in sub section ?
self.dict_to_cfg(file_path=file_path, dict_=self.to_dict())
@staticmethod
def dict_to_cfg(file_path, dict_):
""" """
return ConfigBase._dict_to_cfg(
file_path=file_path,
dict_=dict_,
comments_fct=TomoEDFConfig.get_comments,
logger=_logger,
)
@staticmethod
def from_cfg_file(file_path: str, encoding=None):
assert file_path is not None, "file_path should not be None"
config_parser = configparser.ConfigParser(allow_no_value=True)
config_parser.read(file_path, encoding=encoding)
return TomoEDFConfig.from_dict(config_parser)
@staticmethod
def get_comments(key):
return TomoEDFConfig.COMMENTS[key]
[docs]def generate_default_edf_config(level: str = "required") -> dict:
"""generate a default configuration for converting spec-edf to NXtomo"""
return TomoEDFConfig().to_dict(level=level)