# 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 abc import ABC, abstractmethod
from uuid import UUID
import numpy as np
from ..data import Data, DataAssociationEnum
from ..groups import PropertyGroup
from ..shared.utils import box_intersect, mask_by_extent
from .points import Points
[docs]
class CellObject(Points, ABC):
"""
Base class for object with cells.
:param cells: Array of indices defining connecting vertices.
"""
_attribute_map: dict = Points._attribute_map.copy()
_TYPE_UID: UUID | None = None
def __init__(
self,
cells: np.ndarray | list | tuple | None = None,
**kwargs,
):
super().__init__(**kwargs)
self._cells = self.validate_cells(cells)
@property
def cells(self) -> np.ndarray:
"""
Array of indices defining connecting vertices.
"""
if self._cells is None and self.on_file:
self._cells = self.workspace.fetch_array_attribute(self, "cells")
return self._cells
@cells.setter
def cells(self, cells: np.ndarray | list | tuple):
cells = self.validate_cells(cells)
if self._cells is not None and self._cells.shape != cells.shape:
raise ValueError(
"New cells array must have the same shape as the current cells array."
)
self._cells = cells
self.workspace.update_attribute(self, "cells")
@property
def centroids(self) -> np.ndarray | None:
"""
Compute the centroids of the cells.
"""
return np.mean(self.vertices[self.cells], axis=1)
@property
def locations(self):
return self.vertices
[docs]
def mask_by_extent(
self,
extent: np.ndarray,
inverse: bool = False,
) -> np.ndarray | None:
"""
Extension of :func:`~geoh5py.shared.entity.Entity.mask_by_extent`.
"""
if self.extent is None or not box_intersect(self.extent, extent):
return None
vert_mask = mask_by_extent(self.vertices, extent, inverse=inverse)
# Check for orphan vertices
cell_mask = np.all(vert_mask[self.cells], axis=1)
orphan_mask = np.zeros_like(vert_mask, dtype=bool)
orphan_mask[self.cells[cell_mask].flatten()] = True
vert_mask &= orphan_mask
if ~np.any(vert_mask):
return None
return vert_mask
@property
def n_cells(self) -> int:
"""
Number of vertices
"""
return self.cells.shape[0]
[docs]
def remove_cells(self, indices: list[int] | np.ndarray, clear_cache: bool = False):
"""
Safely remove cells and corresponding data entries.
:param indices: Indices of cells to be removed.
:param clear_cache: Clear cache of data values.
"""
if isinstance(indices, (list, tuple)):
indices = np.array(indices)
if not isinstance(indices, np.ndarray):
raise TypeError("Indices must be a list or numpy array.")
if (
isinstance(self.cells, np.ndarray)
and np.max(indices) > self.cells.shape[0] - 1
):
raise ValueError("Found indices larger than the number of cells.")
cells = np.delete(self.cells, indices, axis=0)
self.load_children_values()
self._cells = self.validate_cells(cells)
self._remove_children_values(
indices, DataAssociationEnum.CELL, clear_cache=clear_cache
)
self.workspace.update_attribute(self, "cells")
[docs]
def remove_vertices(
self, indices: list[int] | np.ndarray, clear_cache: bool = False
):
"""
Safely remove vertices and cells and corresponding data entries.
:param indices: Indices of vertices to be removed.
:param clear_cache: Clear cache of data values.
"""
n_vertices = self.vertices.shape[0]
super().remove_vertices(indices, clear_cache=clear_cache)
vert_index = np.ones(n_vertices, dtype=bool)
vert_index[indices] = False
new_index = np.ones_like(vert_index, dtype=int)
new_index[vert_index] = np.arange(self.vertices.shape[0])
cell_mask = np.where(~np.all(vert_index[self.cells], axis=1))
self._cells = new_index[self.cells]
self.remove_cells(cell_mask)
self.workspace.update_attribute(self, "cells")
[docs]
def copy( # pylint: disable=too-many-branches
self,
parent=None,
*,
copy_children: bool = True,
clear_cache: bool = False,
mask: np.ndarray | None = None,
cell_mask: np.ndarray | None = None,
**kwargs,
):
"""
Sub-class extension of :func:`~geoh5py.objects.points.Points.copy`.
Additions:
cell_mask: Array of indices to sub-sample the input entity cells.
"""
if mask is not None and self.vertices is not None:
if not isinstance(mask, np.ndarray) or mask.shape != (
self.vertices.shape[0],
):
raise ValueError("Mask must be an array of shape (n_vertices,).")
kwargs.update({"vertices": self.vertices[mask, :]})
new_cells = getattr(self, "cells", None)
if mask is not None:
new_id = np.ones_like(mask, dtype=int)
new_id[mask] = np.arange(np.sum(mask))
if cell_mask is None:
cell_mask = np.all(mask[self.cells], axis=1)
new_cells = new_id[self.cells]
if cell_mask is not None and new_cells is not None:
new_cells = new_cells[cell_mask, :]
kwargs.update(
{
"cells": new_cells,
}
)
new_object = super().copy(
parent=parent,
copy_children=False,
clear_cache=clear_cache,
mask=mask,
**kwargs,
)
if copy_children:
children_map = {}
for child in self.children:
if isinstance(child, PropertyGroup):
continue
if isinstance(child, Data):
if child.name in ["A-B Cell ID"]:
continue
child_mask = mask
if (
child.association is DataAssociationEnum.CELL
and cell_mask is not None
):
child_mask = cell_mask
elif child.association is not DataAssociationEnum.VERTEX:
child_mask = None
child_copy = child.copy(
parent=new_object,
clear_cache=clear_cache,
mask=child_mask,
)
else:
child_copy = self.workspace.copy_to_parent(
child, new_object, clear_cache=clear_cache
)
children_map[child.uid] = child_copy.uid
if self.property_groups:
self.workspace.copy_property_groups(
new_object, self.property_groups, children_map
)
return new_object
[docs]
@abstractmethod
def validate_cells(self, indices: list | tuple | np.ndarray | None) -> np.ndarray:
"""
Validate or generate cells defining the connection between vertices.
:param indices: Array of indices. If None provided, the
vertices are connected sequentially.
:return: Array of indices defining connected vertices.
"""