Source code for pacfish.core.Metadata
# SPDX-FileCopyrightText: 2021 International Photoacoustics Standardisation Consortium (IPASC)
# SPDX-FileCopyrightText: 2021 Janek Gröhl
# SPDX-FileCopyrightText: 2021 Lina Hacker
# SPDX-License-Identifier: BSD 3-Clause License
import numpy as np
import numbers
from abc import ABC, abstractmethod
DIMENSIONALITY_STRINGS = ['time', 'space', 'time and space']
"""
The Dimenstionality_STRINGS define what the value space of the metadatum DIMENSIONALITY is.
"""
[docs]class Units:
"""
A list of the SI and compound units that are used in the IPASC format.
"""
NO_UNIT = "N/A"
DIMENSIONLESS_UNIT = "one"
METERS = "m"
RADIANS = "rad"
JOULES = "J"
SECONDS = "s"
KELVIN = "K"
HERTZ = "Hz"
METERS_PER_SECOND = "m/s"
[docs]class MetaDatum(ABC):
"""
This class represents a meta datum.
A meta datum contains all necessary information to fully characterize the meta information
represented by an instance of this class.
"""
def __init__(self, tag: str, minimal: bool, dtype: (type, tuple), unit: str = Units.NO_UNIT):
"""
Instantiates a MetaDatum and sets all relevant values.
Parameters
----------
tag: str
The tag that corresponds to this meta datum.
minimal: bool
Defines if the metadatum is `minimal` (i.e. if is MUST be reported). Without the
minimal parameters, the time series data cannot be reconstructed into an image.
All parameters that are not minimal are interpreted as "report if present".
dtype: type, tuple
The data type of the meta datum. Can either be a single type or a tuple of possible types.
unit: str
The unit associated with this metadatum. Must be one of the strings defined in pacfish.Units.
Raises
------
TypeError:
if one of the parameters is not of the correct type.
"""
if tag is not None and isinstance(tag, str):
self.tag = tag
else:
raise TypeError("tag parameter was not of type 'string'")
if minimal is not None and isinstance(minimal, bool):
self.mandatory = minimal
else:
raise TypeError("mandatory parameter was not of type 'bool'")
if unit is not None and isinstance(unit, str):
self.unit = unit
else:
raise TypeError("unit parameter was not of type 'string'")
self.dtype = dtype
[docs] @abstractmethod
def evaluate_value_range(self, value) -> bool:
"""
Evaluates if a given value fits to the acceptable value range of the MetaDatum.
Parameters
----------
value: object
value to evaluate
Return
------
bool
True if the given value is acceptable for the respective MetaDatum
"""
pass
[docs]class UnconstrainedMetaDatum(MetaDatum):
"""
This MetaDatum has no limitations on the values associated with it.
"""
def __init__(self, tag, minimal, dtype, unit=Units.NO_UNIT):
super().__init__(tag, minimal, dtype, unit)
[docs] def evaluate_value_range(self, value) -> bool:
if value is None:
return False
if not isinstance(value, self.dtype):
raise TypeError("The given value of", self.tag, "was not of the expected data type. Expected ",
self.dtype, "but was",
type(value).__name__)
return True
[docs]class NonNegativeWholeNumber(MetaDatum):
"""
This MetaDatum is defined to be a non-negative whole number.
"""
def __init__(self, tag, minimal, dtype, unit=Units.NO_UNIT):
super().__init__(tag, minimal, dtype, unit)
[docs] def evaluate_value_range(self, value) -> bool:
if value is None:
return False
if not isinstance(value, int):
if not (isinstance(value, np.ndarray) and len(np.shape(value)) == 0):
raise TypeError("The given value of", self.tag, "was not of the expected data type. Expected ",
"int but was", type(value).__name__)
return value >= 0
[docs]class NonNegativeNumbersInArray(MetaDatum):
"""
This MetaDatum is defined to be an array containing non-negative whole numbers.
"""
def __init__(self, tag, minimal, dtype, unit=Units.NO_UNIT):
super().__init__(tag, minimal, dtype, unit)
[docs] def evaluate_value_range(self, value) -> bool:
if value is None:
return False
if not isinstance(value, np.ndarray):
raise TypeError("The given value of", self.tag, "was not of the expected data type. Expected ",
"np.ndarray but was", type(value).__name__)
for number in np.reshape(value, (-1, )):
if number < 0:
return False
return True
[docs]class NumberWithUpperAndLowerLimit(MetaDatum):
"""
This MetaDatum is defined to be a whole number in between a lower and an upper bound (inclusive).
"""
def __init__(self, tag, minimal, dtype, unit=Units.NO_UNIT, lower_limit=-np.inf, upper_limit=np.inf):
super().__init__(tag, minimal, dtype, unit)
self.lower_limit = lower_limit
self.upper_limit = upper_limit
[docs] def evaluate_value_range(self, value) -> bool:
if value is None:
return False
if isinstance(value, np.ndarray):
for item in np.reshape(value, (-1, )):
if not self.lower_limit <= item <= self.upper_limit:
return False
return True
if not isinstance(value, numbers.Number):
raise TypeError("The given value of", self.tag, "was not of the expected data type. Expected ",
"a number or numpy array but was", type(value).__name__)
return self.lower_limit <= value <= self.upper_limit
[docs]class NDimensionalNumpyArray(MetaDatum):
"""
This MetaDatum is defined to be an array of unconstrained numbers.
"""
def __init__(self, tag, minimal, dtype, unit=Units.NO_UNIT, expected_array_dimension=1):
super().__init__(tag, minimal, dtype, unit)
self.expected_array_dimension = expected_array_dimension
[docs] def evaluate_value_range(self, value) -> bool:
if value is None:
return False
if not isinstance(value, np.ndarray):
raise TypeError("A N-Dimensional array must be of type numpy.ndarray, but was", type(value).__name__)
if len(np.shape(np.atleast_1d(value))) != self.expected_array_dimension:
return False
return True
[docs]class NDimensionalNumpyArrayWithMElements(MetaDatum):
"""
This MetaDatum is defined to be an array with a specific dimensionality.
"""
def __init__(self, tag, minimal, dtype, unit=Units.NO_UNIT, expected_array_dimension=1,
elements_per_dimension=None):
super().__init__(tag, minimal, dtype, unit)
self.expected_array_dimension = expected_array_dimension
self.elements_per_dimension = elements_per_dimension
[docs] def evaluate_value_range(self, value) -> bool:
if value is None:
return False
if not isinstance(value, np.ndarray):
raise TypeError("A N-Dimensional array must be of type numpy.ndarray, but was", type(value).__name__)
num_dimensions_correct = len(np.shape(value)) == self.expected_array_dimension
dimension_elements_correct = True
if self.elements_per_dimension is not None:
if len(np.shape(value)) != len(self.elements_per_dimension):
dimension_elements_correct = False
else:
dimension_elements_correct = False not in [val == self.elements_per_dimension[idx] for idx, val in
enumerate(np.shape(value))]
return num_dimensions_correct and dimension_elements_correct
[docs]class NonNegativeNumber(MetaDatum):
"""
This MetaDatum is defined to be a non-negative number.
"""
def __init__(self, tag, minimal, dtype, unit=Units.NO_UNIT):
super().__init__(tag, minimal, dtype, unit)
[docs] def evaluate_value_range(self, value) -> bool:
if value is None:
return False
if not isinstance(value, self.dtype):
if not (isinstance(value, np.ndarray) and len(np.shape(value))==0):
raise TypeError("The given value of", self.tag, "was not of the expected data type. Expected ", self.dtype,
"but was",
type(value).__name__)
return value >= 0.0
[docs]class EnumeratedString(MetaDatum):
"""
This MetaDatum is defined to be a string that must be from a defined list of strings.
"""
def __init__(self, tag, minimal, dtype, unit=Units.NO_UNIT, permissible_strings=None):
super().__init__(tag, minimal, dtype, unit)
self.permissible_strings = permissible_strings
[docs] def evaluate_value_range(self, value) -> bool:
if value is None:
return False
if not isinstance(value, self.dtype):
raise TypeError("The given value of", self.tag, "was not of the expected data type. Expected ", self.dtype, "but was",
type(value).__name__)
if self.permissible_strings is None:
return False
return value in self.permissible_strings
[docs]class MetadataDeviceTags:
"""
This class defines the MetaData that compose all information needed to describe a
digital twin of a photoacoustic device.
It also specifies the naming conventions of the underlying HDF5 data fields.
Furthermore, it is specified if a certain meta datum is minimal or not, the data type
is defined and the units of the metadatum are given.
"""
# General purpose fields
UNIQUE_IDENTIFIER = UnconstrainedMetaDatum("unique_identifier", True, str)
GENERAL = UnconstrainedMetaDatum("general", True, dict)
ILLUMINATORS = UnconstrainedMetaDatum("illuminators", False, dict)
DETECTORS = UnconstrainedMetaDatum("detectors", True, dict)
FIELD_OF_VIEW = NDimensionalNumpyArrayWithMElements("field_of_view", True, np.ndarray, Units.METERS,
expected_array_dimension=1, elements_per_dimension=[6])
NUMBER_OF_ILLUMINATION_ELEMENTS = NonNegativeWholeNumber("num_illuminators", False, int, Units.DIMENSIONLESS_UNIT)
NUMBER_OF_DETECTION_ELEMENTS = NonNegativeWholeNumber("num_detectors", False, int, Units.DIMENSIONLESS_UNIT)
# Illumination geometry-specific fields
ILLUMINATION_ELEMENT = UnconstrainedMetaDatum("illumination_element", False, str)
ILLUMINATOR_POSITION = NDimensionalNumpyArray("illuminator_position", False, np.ndarray, Units.METERS,
expected_array_dimension=1)
ILLUMINATOR_ORIENTATION = NDimensionalNumpyArray("illuminator_orientation", False, np.ndarray, Units.METERS,
expected_array_dimension=1)
ILLUMINATOR_GEOMETRY = UnconstrainedMetaDatum("illuminator_geometry", False, (float, np.ndarray, str),
Units.METERS)
ILLUMINATOR_GEOMETRY_TYPE = UnconstrainedMetaDatum("illuminator_geometry_type", False, str, Units.METERS)
WAVELENGTH_RANGE = NDimensionalNumpyArray("wavelength_range", False, np.ndarray, Units.METERS,
expected_array_dimension=1)
BEAM_ENERGY_PROFILE = NDimensionalNumpyArray("beam_energy_profile", False, np.ndarray, Units.JOULES,
expected_array_dimension=2)
BEAM_STABILITY_PROFILE = NDimensionalNumpyArray("beam_stability_profile", False, np.ndarray, Units.JOULES,
expected_array_dimension=2)
PULSE_WIDTH = NonNegativeNumber("pulse_width", False, float, Units.SECONDS)
BEAM_INTENSITY_PROFILE = NDimensionalNumpyArray("beam_intensity_profile", False, np.ndarray,
Units.DIMENSIONLESS_UNIT,
expected_array_dimension=2)
INTENSITY_PROFILE_DISTANCE = NonNegativeNumber("intensity_profile_distance", False, float, Units.METERS)
BEAM_DIVERGENCE_ANGLES = NumberWithUpperAndLowerLimit("beam_divergence_angles", False, float, Units.RADIANS,
lower_limit=0, upper_limit=2*np.pi)
# Detection geometry-specific fields
DETECTION_ELEMENT = UnconstrainedMetaDatum("detection_element", True, str)
DETECTOR_POSITION = NDimensionalNumpyArray("detector_position", True, np.ndarray, Units.METERS,
expected_array_dimension=1)
DETECTOR_ORIENTATION = NDimensionalNumpyArray("detector_orientation", False, np.ndarray, Units.METERS,
expected_array_dimension=1)
DETECTOR_GEOMETRY = UnconstrainedMetaDatum("detector_geometry", False, (float, np.ndarray, str),
Units.METERS)
DETECTOR_GEOMETRY_TYPE = UnconstrainedMetaDatum("detector_geometry_type", False, str, Units.METERS)
FREQUENCY_RESPONSE = NonNegativeNumbersInArray("frequency_response", False, np.ndarray,
Units.HERTZ + " / " + Units.DIMENSIONLESS_UNIT)
ANGULAR_RESPONSE = NDimensionalNumpyArray("angular_response", False, np.ndarray,
Units.RADIANS + " / " + Units.DIMENSIONLESS_UNIT,
expected_array_dimension=2)
TAGS_GENERAL = [GENERAL, UNIQUE_IDENTIFIER, ILLUMINATORS, DETECTORS, FIELD_OF_VIEW, NUMBER_OF_ILLUMINATION_ELEMENTS,
NUMBER_OF_DETECTION_ELEMENTS]
TAGS_ILLUMINATORS = [ILLUMINATION_ELEMENT, ILLUMINATOR_POSITION, ILLUMINATOR_ORIENTATION, ILLUMINATOR_GEOMETRY,
ILLUMINATOR_GEOMETRY_TYPE,
WAVELENGTH_RANGE, BEAM_ENERGY_PROFILE, BEAM_STABILITY_PROFILE, PULSE_WIDTH,
BEAM_INTENSITY_PROFILE, INTENSITY_PROFILE_DISTANCE, BEAM_DIVERGENCE_ANGLES]
TAGS_DETECTORS = [DETECTION_ELEMENT, DETECTOR_POSITION, DETECTOR_ORIENTATION, DETECTOR_GEOMETRY, FREQUENCY_RESPONSE,
ANGULAR_RESPONSE, DETECTOR_GEOMETRY_TYPE]
TAGS = TAGS_GENERAL + TAGS_DETECTORS + TAGS_ILLUMINATORS
[docs]class MetadataAcquisitionTags:
"""
This class defines the MetaData that compose all information needed to describe the
measurement circumstances for a given measurement of photoacoustic time series data.
It also specifies the naming conventions of the underlying HDF5 data fields.
Furthermore, it is specified if a certain meta datum is minimal or not, the data type
is defined and the units of the metadatum are given.
"""
UUID = UnconstrainedMetaDatum("uuid", True, str)
ENCODING = UnconstrainedMetaDatum("encoding", True, str)
COMPRESSION = UnconstrainedMetaDatum("compression", True, str)
DATA_TYPE = UnconstrainedMetaDatum("data_type", True, str)
DIMENSIONALITY = EnumeratedString("dimensionality", True, str, permissible_strings=DIMENSIONALITY_STRINGS)
SIZES = NonNegativeNumbersInArray("sizes", True, np.ndarray, Units.DIMENSIONLESS_UNIT)
REGIONS_OF_INTEREST = UnconstrainedMetaDatum("regions_of_interest", False, dict, Units.METERS)
PHOTOACOUSTIC_IMAGING_DEVICE_REFERENCE = UnconstrainedMetaDatum("photoacoustic_imaging_device_reference", False, str)
PULSE_ENERGY = NonNegativeNumbersInArray("pulse_energy", False, np.ndarray, Units.JOULES)
MEASUREMENT_TIMESTAMPS = NonNegativeNumbersInArray("measurement_timestamps", False,
np.ndarray, Units.SECONDS)
MEASUREMENT_SPATIAL_POSES = NDimensionalNumpyArray("measurement_spatial_poses", False,
np.ndarray, Units.SECONDS,
expected_array_dimension=2)
ACQUISITION_WAVELENGTHS = NDimensionalNumpyArray("acquisition_wavelengths", False,
np.ndarray, Units.METERS, expected_array_dimension=1)
TIME_GAIN_COMPENSATION = NonNegativeNumbersInArray("time_gain_compensation", False, np.ndarray,
Units.DIMENSIONLESS_UNIT)
OVERALL_GAIN = NonNegativeNumber("overall_gain", False, float, Units.DIMENSIONLESS_UNIT)
ELEMENT_DEPENDENT_GAIN = NonNegativeNumbersInArray("element_dependent_gain", False, np.ndarray,
Units.DIMENSIONLESS_UNIT)
TEMPERATURE_CONTROL = NonNegativeNumbersInArray("temperature_control", False, np.ndarray, Units.KELVIN)
ACOUSTIC_COUPLING_AGENT = UnconstrainedMetaDatum("acoustic_coupling_agent", False, str)
SCANNING_METHOD = UnconstrainedMetaDatum("scanning_method", False, str)
SPEED_OF_SOUND = UnconstrainedMetaDatum("speed_of_sound", False, (np.ndarray, float), Units.METERS_PER_SECOND)
AD_SAMPLING_RATE = NonNegativeNumber("ad_sampling_rate", True, float, Units.HERTZ)
FREQUENCY_DOMAIN_FILTER = UnconstrainedMetaDatum("frequency_domain_filter", False, np.ndarray)
MEASUREMENTS_PER_IMAGE = NonNegativeWholeNumber("measurements_per_image", False, int)
TAGS_BINARY = [DATA_TYPE, DIMENSIONALITY, SIZES]
TAGS_CONTAINER = [UUID, ENCODING, COMPRESSION]
TAGS_ACQUISITION = [PHOTOACOUSTIC_IMAGING_DEVICE_REFERENCE, PULSE_ENERGY, ACQUISITION_WAVELENGTHS,
TIME_GAIN_COMPENSATION, OVERALL_GAIN, ELEMENT_DEPENDENT_GAIN, TEMPERATURE_CONTROL,
ACOUSTIC_COUPLING_AGENT, SCANNING_METHOD, AD_SAMPLING_RATE, FREQUENCY_DOMAIN_FILTER,
SPEED_OF_SOUND, MEASUREMENTS_PER_IMAGE, REGIONS_OF_INTEREST, MEASUREMENT_TIMESTAMPS,
MEASUREMENT_SPATIAL_POSES]
TAGS = TAGS_BINARY + TAGS_ACQUISITION + TAGS_CONTAINER