# Copyright (c) 2024 Mira Geoscience Ltd.
#
# This file is part of geoh5py.
#
# geoh5py is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# geoh5py is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with geoh5py. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import uuid
from typing import TYPE_CHECKING, Literal, cast, get_args
import numpy as np
from ..shared import EntityType
from .color_map import ColorMap
from .geometric_data_constants import GeometricDataConstants
from .primitive_type_enum import PrimitiveTypeEnum
from .reference_value_map import ReferenceValueMap
if TYPE_CHECKING:
from ..workspace import Workspace
from .data import Data # noqa: F401
ColorMapping = Literal[
"linear",
"equal_area",
"logarithmic",
"cdf",
"cumulative_distribution_function",
"missing",
]
[docs]
class DataType(EntityType):
# pylint: disable=too-many-arguments
"""
DataType class.
Controls all the attributes of the data type for displays in Geoscience ANALYST.
:param workspace: An active Workspace.
:param primitive_type: The primitive type of the data.
:param color_map: The colormap used for plotting.
:param hidden: If the data are hidden or not.
:param mapping: The type of color stretching to plot the colormap.
:param number_of_bins: The number of bins used by the histogram.
:param transparent_no_data: If the no data values are displayed as transparent or not.
:param units: The type of the units of the data.
:param value_map: Reference value map for to map index with description.
:param kwargs: Additional keyword arguments to set as attributes
(see :obj:`...shared.entity_type.EntityType`).
"""
_attribute_map = EntityType._attribute_map.copy()
_attribute_map.update(
{
"Hidden": "hidden",
"Mapping": "mapping",
"Number of bins": "number_of_bins",
"Primitive type": "primitive_type",
"Transparent no data": "transparent_no_data",
}
)
def __init__(
self,
workspace: Workspace,
primitive_type: type[Data] | PrimitiveTypeEnum | str | None = None,
color_map: ColorMap | None = None,
hidden: bool = False,
mapping: ColorMapping = "equal_area",
number_of_bins: int | None = None,
transparent_no_data: bool = True,
units: str | None = None,
value_map: dict[int, str] | ReferenceValueMap | None = None,
**kwargs,
):
super().__init__(workspace, **kwargs)
self.color_map = color_map
self.hidden = hidden
self.mapping = mapping
self.number_of_bins = number_of_bins
self.primitive_type = primitive_type # type: ignore
self.transparent_no_data = transparent_no_data
self.units = units
self.value_map = value_map # type: ignore
@classmethod
def _for_geometric_data(cls, workspace: Workspace, uid: uuid.UUID) -> DataType:
"""
Get the data type for geometric data.
:param workspace: An active Workspace.
:param uid: The uid of the existing data type to get.
:return: A new instance of DataType.
"""
geom_primitive_type = GeometricDataConstants.primitive_type()
data_type = cast(DataType, workspace.find_type(uid, DataType))
if data_type is not None:
if not data_type.primitive_type == geom_primitive_type:
raise ValueError(
f"Data type with uid {uid} is not of type {geom_primitive_type}"
)
return data_type
return cls(workspace, uid=uid, primitive_type=geom_primitive_type)
@property
def color_map(self) -> ColorMap | None:
r"""
The Colormap used for plotting
The colormap can be set from a dictionary of sorted values with
corresponding RGBA color.
Or from a numpy array containing the RGBA values.
.. code-block:: python
color_map = {
val_1: [r_1, g_1, b_1, a_1],
...,
val_i: [r_i, g_i, b_i, a_i]
}
It can be set to None if non-existing.
"""
return self._color_map
@color_map.setter
def color_map(self, color_map: ColorMap | dict | np.ndarray | None):
if isinstance(color_map, dict):
color_map = ColorMap(**color_map)
elif isinstance(color_map, np.ndarray):
color_map = ColorMap(values=color_map)
if not isinstance(color_map, (ColorMap, type(None))):
raise TypeError(
f"Input value for 'color_map' must be of type {ColorMap},"
f"numpy.ndarray or dict with 'values'."
)
if isinstance(color_map, ColorMap):
color_map.parent = self
self._color_map: ColorMap | None = color_map
self.workspace.update_attribute(self, "color_map")
[docs]
@classmethod
def for_x_data(cls, workspace: Workspace) -> DataType:
"""
Get the data type for x data.
:param workspace: An active Workspace.
:return: A new instance of DataType.
"""
return cls._for_geometric_data(
workspace, GeometricDataConstants.x_datatype_uid()
)
[docs]
@classmethod
def for_y_data(cls, workspace: Workspace) -> DataType:
"""
Get the data type for y data.
:param workspace: An active Workspace.
:return: A new instance of DataType.
"""
return cls._for_geometric_data(
workspace, GeometricDataConstants.y_datatype_uid()
)
[docs]
@classmethod
def for_z_data(cls, workspace: Workspace) -> DataType:
"""
Get the data type for z data.
:param workspace: An active Workspace.
:return: A new instance of DataType.
"""
return cls._for_geometric_data(
workspace, GeometricDataConstants.z_datatype_uid()
)
@property
def hidden(self) -> bool:
"""
If the data are hidden or not.
"""
return self._hidden
@hidden.setter
def hidden(self, value: bool):
if not isinstance(value, bool) and value != 1 and value != 0:
raise TypeError(f"hidden must be a bool, not {type(value)}")
self._hidden: bool = bool(value)
self.workspace.update_attribute(self, "attributes")
@property
def mapping(self) -> str:
"""
The type of color stretching to plot the colormap.
It chan be one of the following:
'linear', 'equal_area', 'logarithmic', 'cdf', 'missing'
"""
return self._mapping
@mapping.setter
def mapping(self, value: ColorMapping):
if value not in get_args(ColorMapping):
raise ValueError(
f"Mapping {value} was provided but should be one of {get_args(ColorMapping)}"
)
self._mapping: str = value
self.workspace.update_attribute(self, "attributes")
@property
def number_of_bins(self) -> int | None:
"""
The number of bins used by the histogram.
It can be None if no histogram is used.
"""
return self._number_of_bins
@number_of_bins.setter
def number_of_bins(self, n_bins: int | None):
if n_bins is None:
pass
elif not isinstance(n_bins, (int, np.integer)) or n_bins < 1:
raise ValueError(
f"Number of bins should be an integer greater than 0 or None, not {n_bins}"
)
self._number_of_bins: int | None = n_bins
self.workspace.update_attribute(self, "attributes")
@property
def primitive_type(self) -> PrimitiveTypeEnum | None:
"""
The primitive type of the data.
"""
return self._primitive_type
@primitive_type.setter
def primitive_type(self, value: str | type[Data] | PrimitiveTypeEnum | None):
if isinstance(value, str):
value = getattr(PrimitiveTypeEnum, value.replace("-", "_").upper())
elif hasattr(value, "primitive_type"):
value = getattr(value, "primitive_type")()
if not isinstance(value, (PrimitiveTypeEnum, type(None))):
raise ValueError(
f"Primitive type value must be of type {PrimitiveTypeEnum}, find {type(value)}"
)
self._primitive_type = value
@property
def transparent_no_data(self) -> bool:
"""
If the no data values are displayed as transparent or not.
"""
return self._transparent_no_data
@transparent_no_data.setter
def transparent_no_data(self, value: bool):
if not isinstance(value, bool) and value != 1 and value != 0:
raise TypeError(f"transparent_no_data must be a bool, not {type(value)}")
self._transparent_no_data = bool(value)
self.workspace.update_attribute(self, "attributes")
@property
def units(self) -> str | None:
"""
The type of the units of the data.
"""
return self._units
@units.setter
def units(self, unit: str | None):
if not isinstance(unit, (str, type(None))):
raise TypeError(f"units must be a string, not {type(unit)}")
self._units = unit
self.workspace.update_attribute(self, "attributes")
[docs]
@staticmethod
def validate_data_type(workspace: Workspace, attribute_dict: dict):
"""
Get a dictionary of attributes and validate the type of data.
:param workspace: An active Workspace.
:param attribute_dict: A dictionary of attributes of the new Datatype to create.
:return: A new instance of DataType.
"""
entity_type = attribute_dict.get("entity_type")
if entity_type is None:
primitive_type = attribute_dict.get("type")
if primitive_type is not None:
assert (
primitive_type.upper() in PrimitiveTypeEnum.__members__
), f"Data 'type' should be one of {PrimitiveTypeEnum.__members__}"
entity_type = {"primitive_type": primitive_type.upper()}
else:
values = attribute_dict.get("values")
if values is None or (
isinstance(values, np.ndarray)
and np.issubdtype(values.dtype, np.floating)
):
entity_type = {"primitive_type": "FLOAT"}
elif isinstance(values, np.ndarray) and (
np.issubdtype(values.dtype, np.integer)
):
entity_type = {"primitive_type": "INTEGER"}
elif isinstance(values, str) or (
isinstance(values, np.ndarray) and values.dtype.kind in ["U", "S"]
):
entity_type = {"primitive_type": "TEXT"}
elif isinstance(values, np.ndarray) and (values.dtype == bool):
entity_type = {"primitive_type": "BOOLEAN"}
else:
raise NotImplementedError(
"Only add_data values of type FLOAT, INTEGER,"
"BOOLEAN and TEXT have been implemented"
)
elif isinstance(entity_type, EntityType) and (
(entity_type.uid not in getattr(workspace, "_types"))
or (entity_type.workspace != workspace)
):
return entity_type.copy(workspace=workspace)
return entity_type
@property
def value_map(self) -> ReferenceValueMap | None:
r"""
Reference value map for to map index with description.
The value_map can be set from a dictionary of sorted values int
values with text description.
.. code-block:: python
value_map = {
val_1: str_1,
...,
val_i: str_i
}
"""
return self._value_map
@value_map.setter
def value_map(self, value_map: dict[int, str] | ReferenceValueMap | None):
if isinstance(value_map, dict):
value_map = ReferenceValueMap(value_map)
if not isinstance(value_map, (ReferenceValueMap, type(None))):
raise TypeError(
f"'value_map' must be a {dict} or {ReferenceValueMap} or {type(None)}."
)
self._value_map: ReferenceValueMap | None = value_map
self.workspace.update_attribute(self, "value_map")