Source code for geoh5py.objects.object_base

#  Copyright (c) 2022 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 abc import abstractmethod
from datetime import datetime
from typing import TYPE_CHECKING

import numpy as np

from ..data import CommentsData, Data
from ..data.primitive_type_enum import PrimitiveTypeEnum
from ..groups import PropertyGroup
from ..shared import Entity
from ..shared.concatenation import Concatenated
from .object_type import ObjectType

if TYPE_CHECKING:
    from .. import workspace


[docs]class ObjectBase(Entity): """ Object base class. """ _attribute_map = Entity._attribute_map.copy() _attribute_map.update( {"Last focus": "last_focus", "PropertyGroups": "property_groups"} ) def __init__(self, object_type: ObjectType, **kwargs): assert object_type is not None self._entity_type = object_type self._property_groups: list[PropertyGroup] | None = None self._last_focus = "None" self._comments = None # self._clipping_ids: List[uuid.UUID] = [] if not any(key for key in kwargs if key in ["name", "Name"]): kwargs["name"] = type(self).__name__ super().__init__(**kwargs) if self.entity_type.name == "Entity": self.entity_type.name = type(self).__name__
[docs] def add_comment(self, comment: str, author: str = None): """ Add text comment to an object. :param comment: Text to be added as comment. :param author: Name of author or defaults to :obj:`~geoh5py.workspace.workspace.Workspace.contributors`. """ date = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") if author is None: author = ",".join(self.workspace.contributors) comment_dict = {"Author": author, "Date": date, "Text": comment} if self.comments is None: self.add_data( { "UserComments": { "values": [comment_dict], "association": "OBJECT", "entity_type": {"primitive_type": "TEXT"}, } } ) else: self.comments.values = self.comments.values + [comment_dict]
[docs] def add_data(self, data: dict, property_group: str = None) -> Data | list[Data]: """ Create :obj:`~geoh5py.data.data.Data` from dictionary of name and arguments. The provided arguments can be any property of the target Data class. :param data: Dictionary of data to be added to the object, e.g. .. code-block:: python data = { "data_A": { 'values': [v_1, v_2, ...], 'association': 'VERTEX' }, "data_B": { 'values': [v_1, v_2, ...], 'association': 'CELLS' }, } :return: List of new Data objects. """ data_objects = [] for name, attr in data.items(): assert isinstance(attr, dict), ( f"Given value to data {name} should of type {dict}. " f"Type {type(attr)} given instead." ) attr["name"] = name self.validate_data_association(attr) entity_type = self.validate_data_type(attr) kwargs = {"parent": self, "association": attr["association"]} for key, val in attr.items(): if key in ["parent", "association", "entity_type", "type"]: continue kwargs[key] = val data_object = self.workspace.create_entity( Data, entity=kwargs, entity_type=entity_type ) if not isinstance(data_object, Data): continue if property_group is not None: self.add_data_to_group(data_object, property_group) data_objects.append(data_object) if len(data_objects) == 1: return data_objects[0] return data_objects
[docs] def add_data_to_group( self, data: list | Data | uuid.UUID, name: str ) -> PropertyGroup: """ Append data children to a :obj:`~geoh5py.groups.property_group.PropertyGroup` All given data must be children of the parent object. :param data: :obj:`~geoh5py.data.data.Data` object, :obj:`~geoh5py.shared.entity.Entity.uid` or :obj:`~geoh5py.shared.entity.Entity.name` of data. :param name: Name of a :obj:`~geoh5py.groups.property_group.PropertyGroup`. A new group is created if none exist with the given name. :return: The target property group. """ if isinstance(data, list): uids = [] for datum in data: uids += self.reference_to_uid(datum) else: uids = self.reference_to_uid(data) association = None template = self.workspace.get_entity(uids[0])[0] if isinstance(template, Data): association = template.association prop_group = self.find_or_create_property_group( name=name, association=association, property_group_type="Interval table" if isinstance(self, Concatenated) else "Multi-element", ) for uid in uids: assert uid in [ child.uid for child in self.children ], f"Given data with uuid {uid} does not match any known children" if uid not in prop_group.properties: prop_group.properties.append(uid) self.workspace.update_attribute(self, "property_groups") return prop_group
@property def cells(self): """ :obj:`numpy.array` of :obj:`int`: Array of indices defining the connection between :obj:`~geoh5py.objects.object_base.ObjectBase.vertices`. """ ... @property def comments(self): """ Fetch a :obj:`~geoh5py.data.text_data.CommentsData` entity from children. """ for child in self.children: if isinstance(child, CommentsData): return child return None
[docs] @classmethod @abstractmethod def default_type_uid(cls) -> uuid.UUID: ...
@property def entity_type(self) -> ObjectType: """ :obj:`~geoh5py.shared.entity_type.EntityType`: Object type. """ return self._entity_type @property def faces(self): ...
[docs] @classmethod def find_or_create_type( cls, workspace: workspace.Workspace, **kwargs ) -> ObjectType: """ Find or create a type instance for a given object class. :param workspace: Target :obj:`~geoh5py.workspace.workspace.Workspace`. :return: The ObjectType instance for the given object class. """ return ObjectType.find_or_create(workspace, cls, **kwargs)
[docs] def find_or_create_property_group(self, **kwargs) -> PropertyGroup: """ Find or create :obj:`~geoh5py.groups.property_group.PropertyGroup` from given name and properties. :param kwargs: Any arguments taken by the :obj:`~geoh5py.groups.property_group.PropertyGroup` class. :return: A new or existing :obj:`~geoh5py.groups.property_group.PropertyGroup` """ property_groups = [] if self._property_groups is not None: property_groups = self._property_groups if "name" in kwargs and any( pg.name == kwargs["name"] for pg in property_groups ): prop_group = [pg for pg in property_groups if pg.name == kwargs["name"]][0] else: kwargs["parent"] = self prop_group = PropertyGroup(**kwargs) property_groups += [prop_group] self._property_groups = property_groups return prop_group
[docs] def get_data(self, name: str) -> list[Data]: """ Get a child :obj:`~geoh5py.data.data.Data` by name. :param name: Name of the target child data :return: A list of children Data objects """ entity_list = [] for child in self.children: if isinstance(child, Data) and child.name == name: entity_list.append(child) return entity_list
[docs] def get_data_list(self) -> list[str]: """ Get a list of names of all children :obj:`~geoh5py.data.data.Data`. :return: List of names of data associated with the object. """ name_list = [] for child in self.children: if isinstance(child, Data): name_list.append(child.name) return sorted(name_list)
@property def last_focus(self) -> str: """ :obj:`bool`: Object visible in camera on start. """ return self._last_focus @last_focus.setter def last_focus(self, value: str): self._last_focus = value @property def n_cells(self) -> int | None: """ :obj:`int`: Number of cells. """ if self.cells is not None: return self.cells.shape[0] return None @property def n_vertices(self) -> int | None: """ :obj:`int`: Number of vertices. """ if self.vertices is not None: return self.vertices.shape[0] return None @property def property_groups(self) -> list[PropertyGroup] | None: """ :obj:`list` of :obj:`~geoh5py.groups.property_group.PropertyGroup`. """ return self._property_groups @property_groups.setter def property_groups(self, prop_groups: list[PropertyGroup]): # Check for existing property_group if prop_groups is None: property_groups = None else: property_groups = self._property_groups if property_groups is None: property_groups = [] for prop_group in prop_groups: if not any( pg.uid == prop_group.uid for pg in property_groups ) and not any(pg.name == prop_group.name for pg in property_groups): prop_group.parent = self property_groups += [prop_group] self._property_groups = property_groups self.workspace.update_attribute(self, "property_groups") @property def vertices(self): r""" :obj:`numpy.array` of :obj:`float`, shape (\*, 3): Array of x, y, z coordinates defining the position of points in 3D space. """ ...
[docs] def validate_data_association(self, attribute_dict): """ Get a dictionary of attributes and validate the data 'association' keyword. """ if attribute_dict.get("association") is not None: return if ( getattr(self, "n_cells", None) is not None and attribute_dict["values"].ravel().shape[0] == self.n_cells ): attribute_dict["association"] = "CELL" elif ( getattr(self, "n_vertices", None) is not None and attribute_dict["values"].ravel().shape[0] == self.n_vertices ): attribute_dict["association"] = "VERTEX" else: attribute_dict["association"] = "OBJECT"
[docs] @staticmethod def validate_data_type(attribute_dict): """ Get a dictionary of attributes and validate the type of data. """ 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 (values.dtype in [np.float32, np.float64]) ): entity_type = {"primitive_type": "FLOAT"} elif isinstance(values, np.ndarray) and ( values.dtype in [np.uint32, np.int32] ): entity_type = {"primitive_type": "INTEGER"} elif isinstance(values, str): entity_type = {"primitive_type": "TEXT"} else: raise NotImplementedError( "Only add_data values of type FLOAT, INTEGER and TEXT have been implemented" ) return entity_type