# 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 abc import ABC, abstractmethod
from typing import cast
import numpy as np
from ...data import Data, ReferencedData
from ..curve import Curve
from ..object_type import ObjectType
[docs]
class BaseElectrode(Curve, ABC):
_potential_electrodes: PotentialElectrode | None = None
_current_electrodes: CurrentElectrode | None = None
def __init__(self, object_type: ObjectType, **kwargs):
self._metadata: dict | None = None
self._ab_cell_id: ReferencedData | None = None
super().__init__(object_type, **kwargs)
@property
def ab_cell_id(self) -> ReferencedData | None:
"""
Reference data entity mapping cells to a unique current dipole.
"""
if getattr(self, "_ab_cell_id", None) is None:
child = self.get_data("A-B Cell ID")
if any(child) and isinstance(child[0], ReferencedData):
self.ab_cell_id = child[0]
if getattr(self, "_ab_cell_id", None) is not None:
return self._ab_cell_id
return None
@ab_cell_id.setter
def ab_cell_id(self, data: Data | np.ndarray):
if isinstance(data, Data):
if not isinstance(data, ReferencedData):
raise TypeError(f"ab_cell_id must be of type {ReferencedData}")
if data.parent.uid == self.uid:
self._ab_cell_id = data
else:
self._ab_cell_id = cast(ReferencedData, data.copy(parent=self))
else:
if data.dtype != np.int32:
print("ab_cell_id values will be converted to type 'int32'")
if any(self.get_data("A-B Cell ID")):
child = self.get_data("A-B Cell ID")[0]
if isinstance(child, ReferencedData):
child.values = data.astype(np.int32)
else:
complement: CurrentElectrode | PotentialElectrode = (
self.current_electrodes
if isinstance(self, PotentialElectrode)
else self.potential_electrodes
)
if complement is not None and complement.ab_cell_id is not None:
entity_type = complement.ab_cell_id.entity_type
else:
value_map = {ii: str(ii) for ii in range(data.max() + 1)}
value_map[0] = "Unknown"
entity_type = { # type: ignore
"primitive_type": "REFERENCED",
"value_map": value_map,
}
data = self.add_data(
{
"A-B Cell ID": {
"values": data.astype(np.int32),
"association": "CELL",
"entity_type": entity_type,
}
}
)
if isinstance(data, ReferencedData):
self._ab_cell_id = data
@property
def ab_map(self) -> dict | None:
"""
Get the ReferenceData.value_map of the ab_value_id
"""
if isinstance(self.ab_cell_id, ReferencedData):
return self.ab_cell_id.value_map
return None
[docs]
def copy(
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.cell_object.CellObject.copy`.
"""
if parent is None:
parent = self.parent
omit_list = [
"_ab_cell_id",
"_metadata",
"_potential_electrodes",
"_current_electrodes",
]
new_entity = super().copy(
parent=parent,
clear_cache=clear_cache,
copy_children=copy_children,
mask=mask,
cell_mask=cell_mask,
omit_list=omit_list,
**kwargs,
)
if self.cells is not None:
if mask is not None:
cell_mask = np.all(mask[self.cells], axis=1)
else:
cell_mask = np.ones(self.cells.shape[0], dtype=bool)
if self.ab_cell_id is not None and self.ab_cell_id.values is not None:
new_entity.ab_cell_id = self.ab_cell_id.values[cell_mask]
complement: CurrentElectrode | PotentialElectrode = (
self.current_electrodes
if isinstance(self, PotentialElectrode)
else self.potential_electrodes
)
# Set the mask of the complement
if (
new_entity.ab_cell_id is not None
and complement is not None
and complement.ab_cell_id is not None
and complement.ab_cell_id.values is not None
and complement.vertices is not None
and complement.cells is not None
):
intersect = np.intersect1d(
new_entity.ab_cell_id.values,
complement.ab_cell_id.values,
)
cell_mask = np.r_[
[(val in intersect) for val in complement.ab_cell_id.values]
]
# Convert cell indices to vertex indices
mask = np.zeros(complement.vertices.shape[0], dtype=bool)
mask[complement.cells[cell_mask, :]] = True
new_complement = super(Curve, complement).copy( # type: ignore
parent=parent,
omit_list=omit_list,
copy_children=copy_children,
clear_cache=clear_cache,
mask=mask,
cell_mask=cell_mask,
)
if isinstance(self, PotentialElectrode):
new_entity.current_electrodes = new_complement
else:
new_entity.potential_electrodes = new_complement
if new_complement.ab_cell_id is None and complement.ab_cell_id is not None:
new_complement.ab_cell_id = complement.ab_cell_id.values[cell_mask]
# Re-number the ab_cell_id
value_map = {
val: ind
for ind, val in enumerate(
np.r_[0, np.unique(new_entity.current_electrodes.ab_cell_id.values)]
)
}
new_map = {
val: new_entity.current_electrodes.ab_cell_id.value_map.map[val]
for val in value_map.values()
}
new_complement.ab_cell_id.values = np.asarray(
[value_map[val] for val in new_complement.ab_cell_id.values]
)
new_entity.ab_cell_id.values = np.asarray(
[value_map[val] for val in new_entity.ab_cell_id.values]
)
new_entity.ab_cell_id.value_map.map = new_map
return new_entity
@property
@abstractmethod
def current_electrodes(self):
"""
The associated current_electrodes (transmitters)
"""
[docs]
@classmethod
@abstractmethod
def default_type_uid(cls) -> uuid.UUID:
"""Default unique identifier. Implemented on the child class."""
@property
def metadata(self):
"""
Metadata attached to the entity.
"""
if getattr(self, "_metadata", None) is None:
metadata = self.workspace.fetch_metadata(self.uid)
self._metadata = metadata
return self._metadata
@metadata.setter
def metadata(self, values):
if not len(values) == 2:
raise ValueError(
f"Metadata must have two key-value pairs. {values} provided."
)
default_keys = ["Current Electrodes", "Potential Electrodes"]
if list(values.keys()) != default_keys:
raise ValueError(f"Input metadata must have for keys {default_keys}")
if self.workspace.get_entity(values["Current Electrodes"])[0] is None:
raise IndexError("Input Current Electrodes uuid not present in Workspace")
if self.workspace.get_entity(values["Potential Electrodes"])[0] is None:
raise IndexError("Input Potential Electrodes uuid not present in Workspace")
self._metadata = values
self.workspace.update_attribute(self, "metadata")
@property
@abstractmethod
def potential_electrodes(self):
"""
The associated potential_electrodes (receivers)
"""
[docs]
class PotentialElectrode(BaseElectrode):
"""
Ground potential electrode (receiver).
"""
__TYPE_UID = uuid.UUID("{275ecee9-9c24-4378-bf94-65f3c5fbe163}")
@property
def current_electrodes(self):
"""
The associated current electrode object (sources).
"""
if getattr(self, "_current_electrodes", None) is None:
if self.metadata is not None and "Current Electrodes" in self.metadata:
transmitter = self.metadata["Current Electrodes"]
transmitter_entity = self.workspace.get_entity(transmitter)[0]
if isinstance(transmitter_entity, CurrentElectrode):
self._current_electrodes = transmitter_entity
return self._current_electrodes
@current_electrodes.setter
def current_electrodes(self, current_electrodes: CurrentElectrode):
if not isinstance(current_electrodes, CurrentElectrode):
raise TypeError(
f"Provided current_electrodes must be of type {CurrentElectrode}. "
f"{type(current_electrodes)} provided."
)
metadata = {
"Current Electrodes": current_electrodes.uid,
"Potential Electrodes": self.uid,
}
self.metadata = metadata
current_electrodes.metadata = metadata
if isinstance(current_electrodes.ab_cell_id, ReferencedData) and isinstance(
self.ab_cell_id, ReferencedData
):
self.ab_cell_id.entity_type = current_electrodes.ab_cell_id.entity_type
@property
def potential_electrodes(self):
"""
The associated potential_electrodes (receivers)
"""
return self
[docs]
@classmethod
def default_type_uid(cls) -> uuid.UUID:
"""
:return: Default unique identifier
"""
return cls.__TYPE_UID
[docs]
class CurrentElectrode(BaseElectrode):
"""
Ground direct current electrode (transmitter).
"""
__TYPE_UID = uuid.UUID("{9b08bb5a-300c-48fe-9007-d206f971ea92}")
def __init__(self, object_type: ObjectType, **kwargs):
self._current_line_id: uuid.UUID | None = None
super().__init__(object_type, **kwargs)
[docs]
@classmethod
def default_type_uid(cls) -> uuid.UUID:
"""
:return: Default unique identifier
"""
return cls.__TYPE_UID
@property
def current_electrodes(self):
"""
The associated current electrode object (sources).
"""
return self
@current_electrodes.setter
def current_electrodes(self, _):
...
@property
def potential_electrodes(self) -> PotentialElectrode | None:
"""
The associated potential_electrodes (receivers)
"""
if getattr(self, "_potential_electrodes", None) is None:
if self.metadata is not None and "Potential Electrodes" in self.metadata:
potential = self.metadata["Potential Electrodes"]
potential_entity = self.workspace.get_entity(potential)[0]
if isinstance(potential_entity, PotentialElectrode):
self._potential_electrodes = potential_entity
return self._potential_electrodes
@potential_electrodes.setter
def potential_electrodes(self, potential_electrodes: PotentialElectrode):
if not isinstance(potential_electrodes, PotentialElectrode):
raise TypeError(
f"Provided potential_electrodes must be of type {PotentialElectrode}. "
f"{type(potential_electrodes)} provided."
)
metadata = {
"Current Electrodes": self.uid,
"Potential Electrodes": potential_electrodes.uid,
}
self.metadata = metadata
potential_electrodes.metadata = metadata
if isinstance(potential_electrodes.ab_cell_id, ReferencedData) and isinstance(
self.ab_cell_id, ReferencedData
):
potential_electrodes.ab_cell_id.entity_type = self.ab_cell_id.entity_type
[docs]
def add_default_ab_cell_id(self):
"""
Utility function to set ab_cell_id's based on curve cells.
"""
if getattr(self, "cells", None) is None or self.n_cells is None:
raise AttributeError(
"Cells must be set before assigning default ab_cell_id"
)
data = np.arange(self.n_cells) + 1
value_map = {ii: str(ii) for ii in range(self.n_cells + 1)}
value_map[0] = "Unknown"
ab_cell_id = self.add_data(
{
"A-B Cell ID": {
"values": data,
"association": "CELL",
"entity_type": {
"primitive_type": "REFERENCED",
"value_map": value_map,
},
}
}
)
if isinstance(ab_cell_id, ReferencedData):
ab_cell_id.entity_type.name = "A-B"
self._ab_cell_id = ab_cell_id