import logging as log
import ifm_contrib as ifm
import numpy as np
import pyvista as pv
ifm.forceLicense("Viewer")
# log configuration
logger = log.getLogger(__name__)
[docs]def points_and_cells(doc: ifm.FeflowDoc):
"""
Get points and cells in a pyvista compatible format.
:param doc: The FEFLOW data.
:type doc: ifm.FeflowDoc
:return: pts, cells, celltypes (points, cells, celltypes)
:rtype: tuple(numpy.ndarray, list, list)
"""
# 0. define variables
cell_type_dict = {
2: {3: pv.CellType.TRIANGLE, 4: pv.CellType.QUAD},
3: {
4: pv.CellType.TETRA,
6: pv.CellType.WEDGE,
8: pv.CellType.HEXAHEDRON,
},
}
dimension = doc.getNumberOfDimensions()
# 1. get a list of all cells/elements and reverse it for correct node order in OGS
elements = np.fliplr(np.array(doc.c.mesh.get_imatrix())).tolist()
# 2. write the amount of nodes per element to the first entry of each list
# of nodes. This is needed for pyvista !
# 2 could also be done with np.hstack([len(ele)]*len(elements,elements))
# also write the celltypes.
celltypes = []
for element in elements:
nElement = len(element)
element.insert(0, nElement)
celltypes.append(cell_type_dict[dimension][nElement])
# 3. bring the elements to the right format for pyvista
cells = np.array(elements).ravel()
# 4 .write the list for all points and their global coordinates
if dimension == 2:
points = doc.c.mesh.df.nodes(global_cos=True)
pts = points[["X", "Y"]].to_numpy()
# A 0 is appended since in pyvista points must be given in 3D.
# So we set the Z-coordinate to 0.
pts = np.pad(pts, [(0, 0), (0, 1)])
elif dimension == 3:
points = doc.c.mesh.df.nodes(
global_cos=True, par={"Z": ifm.Enum.P_ELEV}
)
pts = points[["X", "Y", "Z"]].to_numpy()
else:
msg = "The input data is neither 2D nor 3D, which it needs to be."
raise ValueError(msg)
# 5. log information
logger.info(
"There are %s number of points and %s number of cells to be converted.",
len(pts),
len(celltypes),
)
return pts, cells, celltypes
def _material_ids_from_selections(doc: ifm.FeflowDoc):
"""
Get MaterialIDs from the FEFLOW data. Only applicable if they are
saved in doc.c.sel.selections().
:param doc: The FEFLOW data.
:type doc: ifm.FeflowDoc
:return: MaterialIDs
:rtype: tuple
"""
# Note: an error occurs if there are no elements defined to the selection
# 1. define necessary variables
mat_ids = []
elements = []
dict_matid = {}
mat_id = 0
# 2. try to overcome ValueError for selec not referring to element
for selec in doc.c.sel.selections():
try:
# 2.1 write a list of elements that refer to a material that
# is defined in selections (mat__elements)
# this call can cause an ValueError since not all selections refer to elements
# but all elemental selection refer to material_ids
elements.append(
doc.c.mesh.df.elements(selection=selec).index.values
)
# 2.2 write a list with a corresponding id for
# that material (mat_id)
mat_ids.append(int(mat_id))
# 2.3 write a dictionary to understand which id refers
# to which selection
dict_matid[mat_id] = selec
mat_id += 1
except ValueError:
pass
# 3. write a list of material ids. The id is written to the corresponding
# entry in the list.
mat_ids_mesh = [0] * doc.getNumberOfElements()
for count, value in enumerate(mat_ids):
for element in elements[count]:
mat_ids_mesh[element] = value
# 4. log the dictionary of the MaterialIDs
logger.info("MaterialIDs refer to: %s", dict_matid)
# MaterialIDs must be int32
return {"MaterialIDs": np.array(mat_ids_mesh).astype(np.int32)}
def _point_and_cell_data(MaterialIDs: dict, doc: ifm.FeflowDoc):
"""
Get point and cell data from Feflow data. Also write the MaterialIDs to the
cell data.
:param doc: The FEFLOW data.
:type doc: ifm.FeflowDoc
:param MaterialIDs:
:type MaterialIDs: dict
:return: pt_data, cell_data (point and cell data)
:rtype: tuple(dict,dict)
"""
# 1. create a dictionary to filter all nodal and elemental values
# according to their name and ifm.Enum value
items_pts = doc.c.mesh.df.get_available_items(Type="nodal")[
"Name"
].to_dict()
items_cell = doc.c.mesh.df.get_available_items(Type="elemental")[
"Name"
].to_dict()
# 2. swap key and values in these dictionaries to have
# the name of the ifm.Enum value as key
pts_dict = {y: x for x, y in items_pts.items()}
cell_dict = {y: x for x, y in items_cell.items()}
# 3. get all the nodal and elemental properties of the mesh as pandas
# Dataframe and drop nans if a column is full of nans for all the properties
pt_prop = doc.c.mesh.df.nodes(global_cos=True, par=pts_dict).dropna(
axis=1, how="all"
)
cell_prop = doc.c.mesh.df.elements(global_cos=True, par=cell_dict).dropna(
axis=1, how="all"
)
# 4. write the pandas Dataframe of nodal and elemental properties to
# a dictionary
pt_data = pt_prop.to_dict("series")
cell_data = cell_prop.to_dict("series")
# 5. change format of cell data to a dictionary of lists
cell_data = {key: [cell_data[key]] for key in cell_data}
# 6. add materialIDs to cell data
cell_data[str(list(MaterialIDs.keys())[0])] = [
list(MaterialIDs.values())[0]
]
# 7. write a list of all properties that have been dropped due to nans
nan_arrays = [
x
for x in list(pts_dict.keys()) or list(cell_dict.keys())
if x not in list(pt_data.keys()) and list(cell_data.keys())
]
# 8. log the data arrays
logger.info(
"These data arrays refer to point data: %s", list(pt_data.keys())
)
logger.info(
"These data arrays refer to cell data: %s", list(cell_data.keys())
)
logger.info(
"These data arrays have been neglected as they are full of nans: %s",
nan_arrays,
)
return pt_data, cell_data
def _convert_to_SI_units(mesh: pv.UnstructuredGrid):
"""
FEFLOW often uses days as unit for time. In OGS SI-units are used. This is why
days must be converted to seconds.
:param mesh: mesh
:type mesh: pyvista.UnstructuredGrid
"""
arrays_to_be_converted = ["TRAF", "IOFLOW", "P_COND"]
for data in list(mesh.point_data) + list(mesh.cell_data):
if any(
to_be_converted in data
for to_be_converted in arrays_to_be_converted
):
mesh[data] *= 1 / 86400
if "4TH" in data or "2ND" in data:
mesh[data] *= -1 / 86400
return mesh
[docs]def convert_geometry_mesh(doc: ifm.FeflowDoc):
"""
Get the geometric construction of the mesh.
:param doc: The FEFLOW data.
:type doc: ifm.FeflowDoc
:return: mesh
:rtype: pyvista.UnstructuredGrid
"""
points, cells, celltypes = points_and_cells(doc)
return pv.UnstructuredGrid(cells, celltypes, points)
[docs]def update_geometry(mesh: pv.UnstructuredGrid, doc: ifm.FeflowDoc):
"""
Update the geometric construction of the mesh with point and cell data.
:param mesh: The mesh to be updated.
:type mesh: pyvista.UnstructuredGrid
:param doc: The FEFLOW data.
:type doc: ifm.FeflowDoc
:return: mesh
:rtype: pyvista.UnstructuredGrid
"""
MaterialIDs = _material_ids_from_selections(doc)
point_data, cell_data = _point_and_cell_data(MaterialIDs, doc)
for i in point_data:
mesh.point_data.update({i: point_data[i]})
for i in cell_data:
mesh.cell_data.update({i: cell_data[i][0]})
return _convert_to_SI_units(mesh)
[docs]def convert_properties_mesh(doc: ifm.FeflowDoc):
"""
Get the mesh with point and cell properties.
:param doc: The FEFLOW data.
:type doc: ifm.FeflowDoc
:return: mesh
:rtype: pyvista.UnstructuredGrid
"""
mesh = convert_geometry_mesh(doc)
update_geometry(mesh, doc)
return mesh