Source code for geoh5py.shared.concatenation.property_group
# 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
from collections.abc import Sequence
from typing import TYPE_CHECKING
from uuid import UUID
import numpy as np
from geoh5py.data import Data
from geoh5py.groups.property_group import GroupTypeEnum, PropertyGroup
if TYPE_CHECKING:
from .concatenator import Concatenator
from .object import ConcatenatedObject
[docs]
class ConcatenatedPropertyGroup(PropertyGroup):
def __init__(self, parent: ConcatenatedObject, **kwargs):
super().__init__(parent, **kwargs)
@property
def concatenator(self) -> Concatenator:
"""
Parental Concatenator entity.
"""
return self._parent.concatenator
[docs]
def is_collocated(
self,
locations: np.ndarray,
collocation_distance: float,
) -> bool:
"""
True if locations are collocated with property group.
:param locations: Locations to check.
:param collocation_distance: tolerance for similarity check.
"""
if (
self.locations is None
or locations.ndim != self.locations.ndim
or len(locations) != len(self.locations)
or not np.allclose(locations, self.locations, atol=collocation_distance)
):
return False
return True
@property
def locations(self) -> np.ndarray:
"""Return depths or intervals array if either exists else None."""
if self.depth_ is not None:
return self.depth_.values
if self.from_ is not None and self.to_ is not None:
return np.c_[self.from_.values, self.to_.values]
return None
@property
def depth_(self):
if (
self.properties is None
or len(self.properties) < 1
or self._property_group_type != GroupTypeEnum.DEPTH
):
return None
data = self.parent.get_data( # pylint: disable=no-value-for-parameter
self.properties[0]
)
if any(data) and isinstance(data[0], Data) and "depth" in data[0].name.lower():
return data[0]
return None
@property
def from_(self):
"""Return the data entities defined the 'from' depth intervals."""
if (
self.properties is None
or len(self.properties) < 1
or self._property_group_type != GroupTypeEnum.INTERVAL
):
return None
data = self.parent.get_data( # pylint: disable=no-value-for-parameter
self.properties[0]
)
if any(data) and isinstance(data[0], Data) and "from" in data[0].name.lower():
return data[0]
return None
@property
def to_(self):
"""Return the data entities defined the 'to' depth intervals."""
if (
self.properties is None
or len(self.properties) < 2
or self._property_group_type != GroupTypeEnum.INTERVAL
):
return None
data = self.parent.get_data( # pylint: disable=no-value-for-parameter
self.properties[1]
)
if any(data) and isinstance(data[0], Data) and "to" in data[0].name.lower():
return data[0]
return None
@property
def parent(self) -> ConcatenatedObject:
return self._parent
@parent.setter
def parent(self, parent):
if self._parent is not None:
raise AttributeError("Cannot change parent of a property group.")
if not hasattr(parent, "add_children"):
raise ValueError(
"The 'parent' of a concatenated data must have an 'add_children' method."
)
parent.add_children([self])
self._parent: ConcatenatedObject = parent
parent.workspace.add_or_update_property_group(self)
def _clear_data_list(
self, data: str | Data | Sequence[str | Data | UUID] | UUID
) -> list[str | Data | UUID]:
"""
Clear the data list of any data that is a depth or from/to data.
:param data: List of data to clear.
:return: List of data with depth and from/to data removed.
"""
if isinstance(data, (str, Data, UUID)):
data = [data]
# avoid suppressing depth and from-to directly
avoid = []
if self.depth_ is not None:
avoid.extend([self.depth_, self.depth_.uid, self.depth_.name])
if self.from_ is not None:
avoid.extend([self.from_, self.from_.uid, self.from_.name])
if self.to_ is not None:
avoid.extend([self.to_, self.to_.uid, self.to_.name])
return [d for d in data if d not in avoid]
def _remove_depth_from_to(self):
"""
Remove the depth or from/to data from the property group
if they are the only properties left.
"""
if (
self._properties is not None
and len(self._properties) == 1
and self.depth_ is not None
):
self.depth_.allow_delete = True
self._properties.remove(self.depth_.uid)
self.parent.remove_children([self.depth_, self])
elif (
self._properties is not None
and len(self._properties) == 2
and self.from_ is not None
and self.to_ is not None
):
self.to_.allow_delete = True
self._properties.remove(self.to_.uid)
self.parent.remove_children([self.to_])
self.from_.allow_delete = True
self._properties.remove(self.from_.uid)
self.parent.remove_children([self.from_, self])
[docs]
def remove_properties(self, data: str | Data | Sequence[str | Data | UUID] | UUID):
"""
Remove data from the properties.
The property group is removed if only the depth or from/to data are left.
:param data: Data to remove.
"""
data = self._clear_data_list(data)
if data:
super().remove_properties(data)
self._remove_depth_from_to()